From 50bb38f68e5950a2f1f5890e4500d6a4feab40f8 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 11:26:12 +0200 Subject: [PATCH 01/61] Add react-router-dom in frontend package.json --- frontend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 7b2747e949..73d3f74ea1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^7.8.0" }, "devDependencies": { "@types/react": "^18.2.15", From e99cd442b4b1ea49cab0415acdef32e2075c9596 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 11:28:41 +0200 Subject: [PATCH 02/61] Add axios to frontend package.json --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index 73d3f74ea1..482faa19d7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^7.8.0" From e1805bc86c1e35943523e382dc305bc412de21fe Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 11:29:58 +0200 Subject: [PATCH 03/61] Add zustand for global state in frontend package.json --- frontend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 482faa19d7..7bef8a51e4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,8 @@ "axios": "^1.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^7.8.0" + "react-router-dom": "^7.8.0", + "zustand": "^5.0.7" }, "devDependencies": { "@types/react": "^18.2.15", From 66beb76a473ac398c2494cf8924fb4cf794bc6ba Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 11:46:40 +0200 Subject: [PATCH 04/61] Remove index.css file from folder and remove import from main.jsx --- frontend/src/index.css | 0 frontend/src/main.jsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 frontend/src/index.css diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 51294f3998..bc52a5633e 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,7 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { App } from "./App.jsx"; -import "./index.css"; + ReactDOM.createRoot(document.getElementById("root")).render( From 0b184df069e0a80daa77fc0e43605bb566310a33 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 12:01:34 +0200 Subject: [PATCH 05/61] Import browserRouter and wrap app in BrowserRouter in main.jsx and define routes in app.jsx --- frontend/src/App.jsx | 13 ++++++++++--- frontend/src/main.jsx | 5 ++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0a24275e6e..fba566e620 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,15 @@ +import { Routes, Route } from 'react-router-dom'; + export const App = () => { return ( - <> -

Welcome to Final Project!

- + + }> + }> + }> + }> + ); }; + +export default App; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index bc52a5633e..052d9c7782 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,10 +1,13 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { BrowserRouter } from 'react-router-dom'; import { App } from "./App.jsx"; ReactDOM.createRoot(document.getElementById("root")).render( - + + + ); From 9fc85ae0c11181de99c93fad2407a9eaccec1961 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 12:04:58 +0200 Subject: [PATCH 06/61] Correct route path --- frontend/src/App.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fba566e620..9e00612a9d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,10 +4,10 @@ export const App = () => { return ( - }> - }> - }> - }> + } /> + } /> + } /> + } /> ); }; From 31050c726fb44ffc296447eb1c0945d05031b289 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 12:26:01 +0200 Subject: [PATCH 07/61] Add components and pages folder. Import pages in app.jsx --- frontend/src/App.jsx | 4 ++++ frontend/src/components/pages/Dashboard.jsx | 0 frontend/src/components/pages/Landing.jsx | 0 frontend/src/components/pages/Login.jsx | 0 frontend/src/components/pages/Signup.jsx | 0 5 files changed, 4 insertions(+) create mode 100644 frontend/src/components/pages/Dashboard.jsx create mode 100644 frontend/src/components/pages/Landing.jsx create mode 100644 frontend/src/components/pages/Login.jsx create mode 100644 frontend/src/components/pages/Signup.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9e00612a9d..76a271bfea 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,8 @@ import { Routes, Route } from 'react-router-dom'; +import Landing from './components/pages/Landing'; +import Signup from './components/pages/Signup'; +import Login from './components/pages/Login'; +import Dashboard from './components/pages/Dashboard'; export const App = () => { diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/components/pages/Login.jsx b/frontend/src/components/pages/Login.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx new file mode 100644 index 0000000000..e69de29bb2 From f576ae212c7ef1495f4cd94934c09ddf7ef649b2 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 15 Aug 2025 12:36:18 +0200 Subject: [PATCH 08/61] Add basic structure to pages --- frontend/src/components/pages/Dashboard.jsx | 5 +++++ frontend/src/components/pages/Landing.jsx | 5 +++++ frontend/src/components/pages/Login.jsx | 5 +++++ frontend/src/components/pages/Signup.jsx | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index e69de29bb2..8ecf14a042 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -0,0 +1,5 @@ +const Dashboard = () => { + return

Dashboard Page

; +}; + +export default Dashboard; \ No newline at end of file diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index e69de29bb2..26b5487619 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -0,0 +1,5 @@ +const Landing = () => { + return

Landing Page

; +}; + +export default Landing; \ No newline at end of file diff --git a/frontend/src/components/pages/Login.jsx b/frontend/src/components/pages/Login.jsx index e69de29bb2..af7bb990de 100644 --- a/frontend/src/components/pages/Login.jsx +++ b/frontend/src/components/pages/Login.jsx @@ -0,0 +1,5 @@ +const Login = () => { + return

Login Page

; +}; + +export default Login; \ No newline at end of file diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index e69de29bb2..19dc37e650 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -0,0 +1,5 @@ +const Signup = () => { + return

Signup Page

; +}; + +export default Signup; \ No newline at end of file From 53f94b4cf6d502b3721e57693fa7b340d749d55d Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 14:32:23 +0200 Subject: [PATCH 09/61] Add basic Signup page layout with form structure --- frontend/src/components/pages/Signup.jsx | 37 ++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 19dc37e650..207b9e1018 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -1,5 +1,38 @@ const Signup = () => { - return

Signup Page

; + return ( +
+

Create Your Account

+
+
+ + +
+
+ + +
+
+ + +
+ +
+

Already have an account? Login

+
+ ); }; -export default Signup; \ No newline at end of file + +export default Signup; From 750ee7c2143fdd4966f97c70af033925ec8f8294 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 14:37:58 +0200 Subject: [PATCH 10/61] Add useState hook for name, email, and password --- frontend/src/components/pages/Signup.jsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 207b9e1018..f3d7e326b6 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -1,4 +1,10 @@ +import { useState } from 'react'; + const Signup = () => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + return (

Create Your Account

@@ -9,6 +15,9 @@ const Signup = () => { id='name' type='text' placeholder='Name' + value={name} + onChange={(e) => setName(e.target.value)} + required />
@@ -17,6 +26,9 @@ const Signup = () => { id='email' type='email' placeholder='Email' + value={email} + onChange={(e) => setEmail(e.target.value)} + required />
@@ -25,6 +37,9 @@ const Signup = () => { id='password' type='password' placeholder='Password' + value={password} + onChange={(e) => setPassword(e.target.value)} + required />
From 0b9bfa6fb83e9fb4ba4f583f7d39c3067356bf39 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 14:43:55 +0200 Subject: [PATCH 11/61] Add handleSubmit function with axios POST to backend API --- frontend/src/components/pages/Signup.jsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index f3d7e326b6..15b808c80c 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -1,14 +1,30 @@ import { useState } from 'react'; +import axios from 'axios'; const Signup = () => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const handleSubmit = async (e) => { + e.preventDefault(); + try { + // Call backend API to create user + const response = await axios.post(`${process.env.REACT_APP_API_URL}/users/signup`, { + name, + email, + password + }); + console.log(response.data); + } catch (err) { + console.error(err); + } + } + return (

Create Your Account

-
+
Date: Mon, 18 Aug 2025 14:49:09 +0200 Subject: [PATCH 12/61] Show loading state in button and display error messages --- frontend/src/components/pages/Signup.jsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 15b808c80c..1e6e26914d 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -5,9 +5,12 @@ const Signup = () => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); + setLoading(true); try { // Call backend API to create user const response = await axios.post(`${process.env.REACT_APP_API_URL}/users/signup`, { @@ -18,6 +21,9 @@ const Signup = () => { console.log(response.data); } catch (err) { console.error(err); + setError(err.response?.data?.message || 'Something went wrong. Please try again.'); + } finally { + setLoading(false); } } @@ -58,7 +64,10 @@ const Signup = () => { required />
- + + {error &&

{error}

}

Already have an account? Login

From 2d49b05af5a66c59b8e721f2660cca60ce39c78a Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 14:53:26 +0200 Subject: [PATCH 13/61] Redirect user to dashboard after successfull signup --- frontend/src/components/pages/Signup.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 1e6e26914d..8387694722 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; const Signup = () => { const [name, setName] = useState(''); @@ -7,6 +8,7 @@ const Signup = () => { const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); + const navigate = useNavigate(); const handleSubmit = async (e) => { e.preventDefault(); @@ -19,6 +21,8 @@ const Signup = () => { password }); console.log(response.data); + // Redirect to login page after successful signup + navigate('/dashboard'); } catch (err) { console.error(err); setError(err.response?.data?.message || 'Something went wrong. Please try again.'); From a09b7a7755d7e183990c18d28c96813eb0b04e24 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 15:33:09 +0200 Subject: [PATCH 14/61] Add dotenv for environment variables and update server.js --- backend/package.json | 5 ++++- backend/server.js | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 08f29f2448..0c1249ad9f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,12 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "dotenv": "^17.2.1", "express": "^4.17.3", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.0", "nodemon": "^3.0.1" } -} \ No newline at end of file +} diff --git a/backend/server.js b/backend/server.js index 070c875189..61236fe6f6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,9 @@ import express from "express"; import cors from "cors"; import mongoose from "mongoose"; +import dotenv from "dotenv"; + +dotenv.config(); const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; mongoose.connect(mongoUrl); From 017e92023baeebbe8313ff2f93ebbc663e69f651 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 15:48:37 +0200 Subject: [PATCH 15/61] Create models folder and User.js file for user schema --- backend/models/User.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/models/User.js diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000000..e69de29bb2 From 53c77eb6eef31ce58a6e8b1cab43dc4ecf6ab433 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 16:11:31 +0200 Subject: [PATCH 16/61] Add User model with schema and accessToken --- backend/models/User.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/models/User.js b/backend/models/User.js index e69de29bb2..92c7c31db9 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -0,0 +1,26 @@ +import mongoose from 'mongoose' +import crypto from 'crypto' + + +const userSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString('hex') + } +}); + + +export const User = mongoose.model("User", userSchema); \ No newline at end of file From 37edf8719737de7bb94e71f2b12e6bbf64d6c632 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 18:09:16 +0200 Subject: [PATCH 17/61] Add user signup route with bcrypt password hashing --- backend/routes/userRoutes.js | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 backend/routes/userRoutes.js diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js new file mode 100644 index 0000000000..369e18bcec --- /dev/null +++ b/backend/routes/userRoutes.js @@ -0,0 +1,40 @@ +import express from 'express' +import bcrypt from 'bcrypt' +import { User } from '../models/user.js' + +const router = express.Router() + + +// POST - Register a new user +router.post("/signup", async (req, res) => { + try { + const { name, email, password } = req.body + const salt = bcrypt.genSaltSync() + const user = new User({ name, email, password: bcrypt.hashSync(password, salt) + }) + + //await to not send response before database finished saving + await user.save() + + res.status(201).json({ + success: true, + message: "User created successfully.", + response: { + id: user._id, + accessToken: user.accessToken + } + }) + + } catch (error) { + console.error("Signup error:", error) + res.status(400).json({ + success: false, + message: "Failed to create user.", + response: error.message + }) + } + }) + + + export default router + \ No newline at end of file From ba30c47b3d9a8f5a0314638497dd2db4fed41527 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 18:13:28 +0200 Subject: [PATCH 18/61] Connect userRoutes to server.js for signup functionality --- backend/server.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/server.js b/backend/server.js index 61236fe6f6..bb5ac662e1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,6 +2,7 @@ import express from "express"; import cors from "cors"; import mongoose from "mongoose"; import dotenv from "dotenv"; +import userRoutes from "./routes/userRoutes.js"; dotenv.config(); @@ -15,6 +16,8 @@ const app = express(); app.use(cors()); app.use(express.json()); +app.use('/users', userRoutes); + app.get("/", (req, res) => { res.send("Hello Technigo!"); }); From e07c4ed8fc41d0277f069c124edbbde05f1b6cf3 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 18 Aug 2025 20:03:38 +0200 Subject: [PATCH 19/61] Add type: module to package.json --- backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/package.json b/backend/package.json index 0c1249ad9f..156019102e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,6 +2,7 @@ "name": "project-final-backend", "version": "1.0.0", "description": "Server part of final project", + "type": "module", "scripts": { "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" From d11c409347e4a0c79b06090a507e1edc6245e783 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Tue, 19 Aug 2025 11:12:02 +0200 Subject: [PATCH 20/61] Change bcrypt import to bcryptjs in userRoutes.js --- backend/routes/userRoutes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 369e18bcec..328ddf779a 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,5 +1,5 @@ import express from 'express' -import bcrypt from 'bcrypt' +import bcrypt from 'bcryptjs' import { User } from '../models/user.js' const router = express.Router() From e1ad8f439c1d124bce596db7d30b9d7025acd3b9 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Tue, 19 Aug 2025 14:52:19 +0200 Subject: [PATCH 21/61] Add console.log to verify REACT_APP_API_URL and update .env --- frontend/src/components/pages/Signup.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 8387694722..eea8e75cdf 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -13,6 +13,10 @@ const Signup = () => { const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); + + // Log the API URL to verify it's being read correctly + console.log('API URL:', process.env.REACT_APP_API_URL); + try { // Call backend API to create user const response = await axios.post(`${process.env.REACT_APP_API_URL}/users/signup`, { From 78b4c394647dd31e5905c1b8e0c6a8d54fbc769d Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Tue, 19 Aug 2025 15:55:40 +0200 Subject: [PATCH 22/61] Hardcode API URL Signup.jsx for testing --- frontend/src/components/pages/Signup.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index eea8e75cdf..8bda01915a 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -13,13 +13,14 @@ const Signup = () => { const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); - + + const apiUrl = 'http://localhost:8081'; // Log the API URL to verify it's being read correctly - console.log('API URL:', process.env.REACT_APP_API_URL); + console.log('API URL:', apiUrl); try { // Call backend API to create user - const response = await axios.post(`${process.env.REACT_APP_API_URL}/users/signup`, { + const response = await axios.post(`${apiUrl}/users/signup`, { name, email, password From 969b3d13d5518bdae8bf7352ee90aabd0f201c53 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Tue, 19 Aug 2025 16:16:59 +0200 Subject: [PATCH 23/61] Revert to using process.env.REACT_APP_API_URL in Signup.jsx --- frontend/src/components/pages/Signup.jsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 8bda01915a..321d137b88 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -13,14 +13,10 @@ const Signup = () => { const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); - - const apiUrl = 'http://localhost:8081'; - // Log the API URL to verify it's being read correctly - console.log('API URL:', apiUrl); - + try { // Call backend API to create user - const response = await axios.post(`${apiUrl}/users/signup`, { + const response = await axios.post(`${process.env.REACT_APP_API_URL}/users/signup`, { name, email, password From e900f2c3281671c67ca14ab272567b55ab7a0937 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Tue, 19 Aug 2025 20:09:24 +0200 Subject: [PATCH 24/61] Fix User import and update frontend to use env backend URL --- backend/routes/userRoutes.js | 2 +- frontend/src/components/pages/Signup.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 328ddf779a..720095706b 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,6 +1,6 @@ import express from 'express' import bcrypt from 'bcryptjs' -import { User } from '../models/user.js' +import { User } from '../models/User.js' const router = express.Router() diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 321d137b88..059fb4194f 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -16,7 +16,7 @@ const Signup = () => { try { // Call backend API to create user - const response = await axios.post(`${process.env.REACT_APP_API_URL}/users/signup`, { + const response = await axios.post(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/users/signup`, { name, email, password From d74e263be0a4cceb5fd09fe879e414a7d2ff645f Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 20 Aug 2025 13:13:55 +0200 Subject: [PATCH 25/61] Add express-list-endpoints in package.json, import listendpoints and endpoints of the API in server.js, add authmiddleware.js and jsonwebtoken --- backend/middleware/authMiddleware.js | 22 ++++++++++++++++++++++ backend/package.json | 1 + backend/routes/userRoutes.js | 23 +++++++++++++++++------ backend/server.js | 14 +++++++++++++- 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 backend/middleware/authMiddleware.js diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 0000000000..2c58ad1a81 --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,22 @@ +import { User } from "../models/User.js" + + export const authenticateUser = async (req, res, next) => { + try { + const accessToken = req.header("Authorization") + const user = await User.findOne({ accessToken: accessToken }) + if (user) { + req.user = user + next() + } else { + res.status(401).json({ + message: "Authentication missing or invalid.", + loggedOut: true + }) + } + } catch (error) { + res.status(500).json({ + message: "Internal server error", + error: error.message + }); + } + } \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 156019102e..585ccb1439 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,6 +17,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.0", "nodemon": "^3.0.1" diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 720095706b..f27d75b64a 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,27 +1,38 @@ -import express from 'express' -import bcrypt from 'bcryptjs' -import { User } from '../models/User.js' +import express from 'express'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import { User } from '../models/User.js'; +import dotenv from 'dotenv'; -const router = express.Router() +dotenv.config(); // Load environment variables +console.log('JWT_SECRET:', process.env.JWT_SECRET); +const router = express.Router() +const JWT_SECRET = process.env.JWT_SECRET || 'your_secure_secret_key'; // Use environment variable for the secret key // POST - Register a new user router.post("/signup", async (req, res) => { try { const { name, email, password } = req.body const salt = bcrypt.genSaltSync() - const user = new User({ name, email, password: bcrypt.hashSync(password, salt) + const hashedPassword = bcrypt.hashSync(password, salt); + const user = new User({ name, email, password: hashedPassword }) //await to not send response before database finished saving await user.save() + + // Generate a JWT token + const token = jwt.sign({ id: user._id, email: user.email }, JWT_SECRET, { + expiresIn: '1h', // Token expires in 1 hour + }); res.status(201).json({ success: true, message: "User created successfully.", response: { id: user._id, - accessToken: user.accessToken + accessToken: token, } }) diff --git a/backend/server.js b/backend/server.js index bb5ac662e1..569cbfcccf 100644 --- a/backend/server.js +++ b/backend/server.js @@ -3,8 +3,10 @@ import cors from "cors"; import mongoose from "mongoose"; import dotenv from "dotenv"; import userRoutes from "./routes/userRoutes.js"; +import listEndpoints from "express-list-endpoints"; -dotenv.config(); +dotenv.config(); +console.log('JWT_SECRET:', process.env.JWT_SECRET); const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; mongoose.connect(mongoUrl); @@ -16,6 +18,16 @@ const app = express(); app.use(cors()); app.use(express.json()); +// endpoint for documentation of the API +app.get("/", (req, res) => { + const endpoints = listEndpoints(app) + res.json({ + message: "Welcome to StoryBudget API", + endpoints: endpoints + }) +}) + +//endpoint routes app.use('/users', userRoutes); app.get("/", (req, res) => { From bcee90d3b6c9f7be94ea9aac926d4d0ce3cd7de3 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 20 Aug 2025 14:05:09 +0200 Subject: [PATCH 26/61] Add localStorage and store accessToken --- frontend/src/components/pages/Signup.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 059fb4194f..417c380e48 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -22,6 +22,10 @@ const Signup = () => { password }); console.log(response.data); + + // Store the accessToken in localStorage + localStorage.setItem('accessToken', response.data.response.accessToken); + // Redirect to login page after successful signup navigate('/dashboard'); } catch (err) { From 27c52e8e9e6a104ff69df5faba4d4f9f5c3ff35a Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 20 Aug 2025 19:29:14 +0200 Subject: [PATCH 27/61] Implement login functionality and improve signup validation --- backend/routes/userRoutes.js | 61 +++++++++++++++++++++ backend/server.js | 4 -- frontend/src/components/pages/Login.jsx | 71 ++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index f27d75b64a..167abf9d30 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -14,6 +14,14 @@ const JWT_SECRET = process.env.JWT_SECRET || 'your_secure_secret_key'; // Use en router.post("/signup", async (req, res) => { try { const { name, email, password } = req.body + + if (!name || !email || !password) { + return res.status(400).json({ + success: false, + message: "Name, email, and password are required.", + }); + } + const salt = bcrypt.genSaltSync() const hashedPassword = bcrypt.hashSync(password, salt); const user = new User({ name, email, password: hashedPassword @@ -47,5 +55,58 @@ router.post("/signup", async (req, res) => { }) + // POST - Login route +router.post("/login", async (req, res) => { + try { + const { email, password } = req.body + + if (!email || !password) { + return res.status(400).json({ + success: false, + message: "Email and password are required.", + }); + } + + // Find user by email + const user = await User.findOne({ email }) + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found with provided email.", + }) + } + + // Compare the provided password with the stored hashed password + const isPasswordValid = bcrypt.compareSync(password, user.password) + if (!isPasswordValid) { + return res.status(401).json({ + success: false, + message: "Incorrect password. Please try again.", + }) + } + // Generate a JWT token + const token = jwt.sign({ id: user._id, email: user.email }, JWT_SECRET, { + expiresIn: '1h', // Token expires in 1 hour + }) + + res.status(200).json({ + success: true, + message: "Login successfull.", + response: { + id: user._id, + accessToken: token, + } + }) + + } catch (error) { + console.error("Login error:", error) + res.status(500).json({ + success: false, + message: "Failed to login. Please try again.", + response: error.message + }) + } + }) + export default router \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 569cbfcccf..849b9e16a0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -30,10 +30,6 @@ app.get("/", (req, res) => { //endpoint routes app.use('/users', userRoutes); -app.get("/", (req, res) => { - res.send("Hello Technigo!"); -}); - // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); diff --git a/frontend/src/components/pages/Login.jsx b/frontend/src/components/pages/Login.jsx index af7bb990de..0ce550011a 100644 --- a/frontend/src/components/pages/Login.jsx +++ b/frontend/src/components/pages/Login.jsx @@ -1,5 +1,74 @@ +import { useState } from 'react'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; + const Login = () => { - return

Login Page

; + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + + try { + // Call backend API to create user + const response = await axios.post(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/users/login`, { + email, + password + }); + console.log(response.data); + + // Store the accessToken in localStorage + localStorage.setItem('accessToken', response.data.response.accessToken); + + // Redirect to login page after successful login + navigate('/dashboard'); + } catch (err) { + console.error(err); + setError(err.response?.data?.message || 'Invalid email or password. Please try again.'); + } finally { + setLoading(false); + } + } + + return ( +
+

Login

+
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ + {error &&

{error}

} +
+

Don't have an account? Sign up

+
+ ); }; + export default Login; \ No newline at end of file From 18b170899fc5027f4f2c08ad6734ab3c1576ad4e Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 20 Aug 2025 19:41:38 +0200 Subject: [PATCH 28/61] Install styled-components and set up theme structure --- frontend/package.json | 1 + frontend/src/styles/theme.js | 0 package.json | 5 ++++- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 frontend/src/styles/theme.js diff --git a/frontend/package.json b/frontend/package.json index 7bef8a51e4..0d590a8d9b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^7.8.0", + "styled-components": "^6.1.19", "zustand": "^5.0.7" }, "devDependencies": { diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/package.json b/package.json index 680d190772..ec1b8616b1 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,8 @@ "version": "1.0.0", "scripts": { "postinstall": "npm install --prefix backend" + }, + "dependencies": { + "styled-components": "^6.1.19" } -} \ No newline at end of file +} From 138be2a4999ae1e76b7a854dae0029c4f333c292 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 20 Aug 2025 20:16:32 +0200 Subject: [PATCH 29/61] Add theme and themeProvider in App.jsx. Add theme.js and themes --- frontend/src/App.jsx | 4 ++++ frontend/src/styles/theme.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 76a271bfea..996bc7340e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,3 +1,5 @@ +import { ThemeProvider } from 'styled-components'; +import { theme } from './styles/theme'; import { Routes, Route } from 'react-router-dom'; import Landing from './components/pages/Landing'; import Signup from './components/pages/Signup'; @@ -7,12 +9,14 @@ import Dashboard from './components/pages/Dashboard'; export const App = () => { return ( + } /> } /> } /> } /> + ); }; diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js index e69de29bb2..ef527fd0a5 100644 --- a/frontend/src/styles/theme.js +++ b/frontend/src/styles/theme.js @@ -0,0 +1,24 @@ +export const theme = { + colors: { + primary: '#181A4D', // Main blue color for background and text + primaryLight: 'rgba(24, 26, 77, 0.5)', // 50% opacity for minor parts + black: '#1E1E1E', // Black color for text, icons, and footer backgrounds + white: '#FFFFFF', // White color for background, text, and icons + grey: '#D3DDE7', // Grey color for smaller backgrounds, placeholder text, dividers + greyLight: 'rgba(211, 221, 231, 0.5)', // 50% opacity for minor grey parts + limeGreen: '#D9E73C', // Lime green for visuals + limeGreenLight: 'rgba(217, 231, 60, 0.5)', // 50% opacity for lime green backgrounds + limeGreenExtraLight: 'rgba(217, 231, 60, 0.25)', // 25% opacity for lime green visuals + pink: '#E6BFFF', // Pink for visuals + pinkLight: 'rgba(230, 191, 255, 0.5)', // 50% opacity for pink backgrounds + pinkExtraLight: 'rgba(230, 191, 255, 0.25)', // 25% opacity for pink visuals + }, + typography: { + fontFamily: '"Roboto", sans-serif', // Main font family + fontSize: '16px', // Base font size + fontWeightRegular: 400, // Regular font weight + fontWeightMedium: 500, // Medium font weight + fontWeightBold: 700, // Bold font weight + }, + spacing: (factor) => `${0.25 * factor}rem`, // Spacing utility (e.g., spacing(4) = 1rem) +}; \ No newline at end of file From d2e5ccb1f672544c2dfaf5f9a77fe293265d2083 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 20 Aug 2025 20:36:13 +0200 Subject: [PATCH 30/61] Add globalStyles.js to styles folder.Import globalStyles in App.jsx and styles in globalStle.js --- frontend/src/App.jsx | 14 ++++++----- frontend/src/styles/globalStyles.js | 37 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 frontend/src/styles/globalStyles.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 996bc7340e..35f3df9e75 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import { ThemeProvider } from 'styled-components'; import { theme } from './styles/theme'; +import { GlobalStyles } from './styles/globalStyles'; import { Routes, Route } from 'react-router-dom'; import Landing from './components/pages/Landing'; import Signup from './components/pages/Signup'; @@ -10,12 +11,13 @@ export const App = () => { return ( - - } /> - } /> - } /> - } /> - + + + } /> + } /> + } /> + } /> + ); }; diff --git a/frontend/src/styles/globalStyles.js b/frontend/src/styles/globalStyles.js new file mode 100644 index 0000000000..c8afc57b0d --- /dev/null +++ b/frontend/src/styles/globalStyles.js @@ -0,0 +1,37 @@ +import { createGlobalStyle } from 'styled-components'; + +export const GlobalStyles = createGlobalStyle` + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: ${({ theme }) => theme.typography.fontFamily}; + background-color: ${({ theme }) => theme.colors.background}; + color: ${({ theme }) => theme.colors.text}; + line-height: 1.6; + } + + h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + } + + p { + margin: 0; + } + + button { + font-family: ${({ theme }) => theme.typography.fontFamily}; + cursor: pointer; + border: none; + outline: none; + } + + a { + text-decoration: none; + color: inherit; + } +`; \ No newline at end of file From f2b7c232a82d0271f11df53e014eb387847311a9 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Thu, 21 Aug 2025 20:23:12 +0200 Subject: [PATCH 31/61] Add Mascot.svg and basic styling --- frontend/src/assets/images/Mascot.svg | 164 ++++++++++++++++++++++ frontend/src/components/pages/Landing.jsx | 106 +++++++++++++- 2 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/images/Mascot.svg diff --git a/frontend/src/assets/images/Mascot.svg b/frontend/src/assets/images/Mascot.svg new file mode 100644 index 0000000000..66ac1aeb35 --- /dev/null +++ b/frontend/src/assets/images/Mascot.svg @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index 26b5487619..8c923e5872 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -1,5 +1,107 @@ +import styled from "styled-components"; +import { useNavigate } from "react-router-dom"; + +const LandingContainer = styled.div` + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + font-family: ${({ theme }) => theme.typography.fontFamily}; + height: 100vh; + display: flex; + flex-direction: column; + position: relative; +`; + +const Navbar = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${({ theme }) => theme.spacing(4)}; + border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; // Light grey border for navbar + background-color: ${({ theme }) => theme.colors.primary}; + width: 100%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const Logo = styled.h1` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + color: ${({ theme }) => theme.colors.white}; + margin: 0; +`; + +const NavButtons = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; +`; + +const Button = styled.button` + background-color: ${({ theme }) => theme.colors.pink}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.colors.limeGreenLight}; + } +`; + +const Content = styled.div` + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + text-align: center; + padding: ${({ theme }) => theme.spacing(4)}; + margin-bottom: ${({ theme }) => theme.spacing(8)}; // Push the content up + margin-top: ${({ theme }) => theme.spacing(28)}; // Add space below the navbar + z-index: 2; // Ensure content is above the falling star +`; + +const Title = styled.h2` + font-size: 2rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const Text = styled.p` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightRegular}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const LimeButton = styled(Button)` + margin-top: ${({ theme }) => theme.spacing(4)}; + padding: ${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(6)}; + font-size: 1rem; + background-color: ${({ theme }) => theme.colors.limeGreen}; + color: ${({ theme }) => theme.colors.black}; +`; + const Landing = () => { - return

Landing Page

; + const navigate = useNavigate(); + + return ( + + + StoryBudget + + + + + + + Welcome To StoryBudget + Manage your budgets effortlessly. + navigate('/signup')}>Get Started + + + ) }; -export default Landing; \ No newline at end of file +export default Landing; From c8e4b41bd5250b9b02ad350da5a3e789e6cfdde2 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Thu, 21 Aug 2025 21:09:23 +0200 Subject: [PATCH 32/61] Import Mascot and style marcot.img, mascotcontainer. Add StarsContainer and stars with keyframe animation --- frontend/src/components/pages/Landing.jsx | 114 +++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index 8c923e5872..626381b75d 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -1,5 +1,6 @@ import styled from "styled-components"; import { useNavigate } from "react-router-dom"; +import Mascot from '../../assets/images/Mascot.svg'; const LandingContainer = styled.div` background-color: ${({ theme }) => theme.colors.primary}; @@ -32,6 +33,10 @@ const Logo = styled.h1` const NavButtons = styled.div` display: flex; gap: ${({ theme }) => theme.spacing(4)}; + + @media (max-width: 767px) { + display: none; // Hide buttons on small screens + } `; const Button = styled.button` @@ -77,13 +82,106 @@ const LimeButton = styled(Button)` margin-top: ${({ theme }) => theme.spacing(4)}; padding: ${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(6)}; font-size: 1rem; - background-color: ${({ theme }) => theme.colors.limeGreen}; + background-color: ${({ theme }) => theme.colors.limeGreen}; color: ${({ theme }) => theme.colors.black}; `; +const MascotContainer = styled.div` + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100vw; + height: calc(100vw / 2); + display: flex; + justify-content: center; + align-items: flex-end; + z-index: 1; +`; + +const MascotImage = styled.img` + position: fixed; + bottom: 25%; + left: 50%; + transform: translate(-50%, 0) rotate(45deg); + width: 180px; // Adjust mascot size + height: auto; + animation: float 12s ease-in-out infinite; + + @keyframes float { + 0% { + transform: translate(-50%, 0) rotate(30deg) translateY(0); // Start position + } + 25% { + transform: translate(-50%, -5px) rotate(32deg) translateY(-5px); // Slight upward movement and rotation + } + 50% { + transform: translate(-50%, 0) rotate(30deg) translateY(0); // Float higher + } + 75% { + transform: translate(-50%, 5px) rotate(26deg) translateY(-5px); // Slight downward movement and rotation + } + 100% { + transform: translate(-50%, 0) rotate(30deg) translateY(0); // Return to start + } + } +`; + +const StarsContainer = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; // Prevent interaction with stars + z-index: 1; // Ensure stars are behind the content +`; + +const Star = styled.div` + position: absolute; + width: ${({ size }) => size}px; + height: ${({ size }) => size}px; + background-color: transparent; + clip-path: polygon( + 50% 0%, + 61% 35%, + 98% 35%, + 68% 57%, + 79% 91%, + 50% 70%, + 21% 91%, + 32% 57%, + 2% 35%, + 39% 35% + ); // Creates a star shape + background-color: white; + animation: twinkle ${({ duration }) => duration}s infinite ease-in-out; + top: ${({ top }) => top}%; + left: ${({ left }) => left}%; + + @keyframes twinkle { + 0%, 100% { + opacity: 0.2; + } + 50% { + opacity: 1; + } + } +`; + const Landing = () => { const navigate = useNavigate(); + // Generate random stars + const stars = Array.from({ length: 50 }, (_, i) => ({ + id: i, + size: Math.random() * 3 + 2, // Random size between 2px and 5px + duration: Math.random() * 3 + 2, // Random animation duration between 2s and 5s + top: Math.random() * 100, // Random position (top) + left: Math.random() * 100, // Random position (left) + })); + return ( @@ -95,11 +193,25 @@ const Landing = () => { + + {stars.map((star) => ( + + ))} + Welcome To StoryBudget Manage your budgets effortlessly. navigate('/signup')}>Get Started + + + ) }; From 0e4f2ddaafdc8d8d4f93a28537ab0ccbc287c26e Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Thu, 21 Aug 2025 21:12:21 +0200 Subject: [PATCH 33/61] Add FallingStar and keyframe animation --- frontend/src/components/pages/Landing.jsx | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index 626381b75d..9d211e635c 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -170,6 +170,45 @@ const Star = styled.div` } `; +const FallingStar = styled.div` + position: absolute; + width: 4px; + height: 4px; + background-color: white; + border-radius: 50%; + box-shadow: 0 0 8px white, 0 0 16px white, 0 0 24px white; // Glow effect + animation: fall 14s infinite ease-in-out; + top: -10%; + left: -10%; + z-index: 0; // Ensure falling star is behind the content + + &::after { + content: ''; + position: absolute; + height: 2px; // Thickness of the tail + background: linear-gradient(90deg, rgba(151, 151, 151, 0.8), rgba(255, 255, 255, 0)); // Fading tail effect + top: 50%; + left: -60px; // Position the tail behind the star + transform: translateY(-50%); + border-radius: 50px; // Curve the tail + } + + @keyframes fall { + 0% { + transform: translate(0vw, -10vh) rotate(45deg); + opacity: 1; + } + 70% { + opacity: 1; + } + 100% { + transform: translate(90vw, 120vh) rotate(45deg); + opacity: 0; + } + } +`; + + const Landing = () => { const navigate = useNavigate(); @@ -203,6 +242,7 @@ const Landing = () => { left={star.left} /> ))} + {/* Single falling star */} Welcome To StoryBudget From 6d0d5f4412e45fe2539a320938a10a34cb780bc5 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 22 Aug 2025 19:35:46 +0200 Subject: [PATCH 34/61] Change and add additional styling to landing page --- frontend/src/components/pages/Landing.jsx | 188 ++++++++++++---------- 1 file changed, 102 insertions(+), 86 deletions(-) diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index 9d211e635c..03686a796a 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -4,12 +4,15 @@ import Mascot from '../../assets/images/Mascot.svg'; const LandingContainer = styled.div` background-color: ${({ theme }) => theme.colors.primary}; - color: ${({ theme }) => theme.colors.white}; - font-family: ${({ theme }) => theme.typography.fontFamily}; - height: 100vh; + color: ${({ theme }) => theme.colors.white}; + font-family: ${({ theme }) => theme.typography.fontFamily}; display: flex; flex-direction: column; + align-items: center; + width: 100%; + height: 100vh; position: relative; + overflow: hidden; /* Ensure stars don't overflow */ `; const Navbar = styled.nav` @@ -17,10 +20,11 @@ const Navbar = styled.nav` justify-content: space-between; align-items: center; padding: ${({ theme }) => theme.spacing(4)}; - border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; // Light grey border for navbar + border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; background-color: ${({ theme }) => theme.colors.primary}; width: 100%; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 3; `; const Logo = styled.h1` @@ -53,80 +57,6 @@ const Button = styled.button` } `; -const Content = styled.div` - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - text-align: center; - padding: ${({ theme }) => theme.spacing(4)}; - margin-bottom: ${({ theme }) => theme.spacing(8)}; // Push the content up - margin-top: ${({ theme }) => theme.spacing(28)}; // Add space below the navbar - z-index: 2; // Ensure content is above the falling star -`; - -const Title = styled.h2` - font-size: 2rem; - font-weight: ${({ theme }) => theme.typography.fontWeightBold}; - margin-bottom: ${({ theme }) => theme.spacing(4)}; -`; - -const Text = styled.p` - font-size: 1rem; - font-weight: ${({ theme }) => theme.typography.fontWeightRegular}; - margin-bottom: ${({ theme }) => theme.spacing(4)}; -`; - -const LimeButton = styled(Button)` - margin-top: ${({ theme }) => theme.spacing(4)}; - padding: ${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(6)}; - font-size: 1rem; - background-color: ${({ theme }) => theme.colors.limeGreen}; - color: ${({ theme }) => theme.colors.black}; -`; - -const MascotContainer = styled.div` - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 100vw; - height: calc(100vw / 2); - display: flex; - justify-content: center; - align-items: flex-end; - z-index: 1; -`; - -const MascotImage = styled.img` - position: fixed; - bottom: 25%; - left: 50%; - transform: translate(-50%, 0) rotate(45deg); - width: 180px; // Adjust mascot size - height: auto; - animation: float 12s ease-in-out infinite; - - @keyframes float { - 0% { - transform: translate(-50%, 0) rotate(30deg) translateY(0); // Start position - } - 25% { - transform: translate(-50%, -5px) rotate(32deg) translateY(-5px); // Slight upward movement and rotation - } - 50% { - transform: translate(-50%, 0) rotate(30deg) translateY(0); // Float higher - } - 75% { - transform: translate(-50%, 5px) rotate(26deg) translateY(-5px); // Slight downward movement and rotation - } - 100% { - transform: translate(-50%, 0) rotate(30deg) translateY(0); // Return to start - } - } -`; - const StarsContainer = styled.div` position: absolute; top: 0; @@ -208,6 +138,90 @@ const FallingStar = styled.div` } `; +const Title = styled.h2` + font-size: 2rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; + + @media (min-width: 768px) { + font-size: 2.5rem; + } +`; + +const Text = styled.p` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightRegular}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const LimeButton = styled(Button)` + margin-top: ${({ theme }) => theme.spacing(4)}; + padding: ${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(6)}; + font-size: 1rem; + background-color: ${({ theme }) => theme.colors.limeGreen}; + color: ${({ theme }) => theme.colors.black}; + + &:hover { + background-color: ${({ theme }) => theme.colors.limeGreenLight}; + } +`; + +const ContentContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + width: 100%; + position: relative; + z-index: 2; // Ensure content is above stars + `; + +const Section = styled.section` + flex: 1; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + text-align: center; + padding: ${({ theme }) => theme.spacing(8)}; + + @media (min-width: 768px) { + padding: ${({ theme }) => theme.spacing(12)}; + } +`; + +const MascotSection = styled(Section)` + flex: 1; + display: flex; + justify-content: flex-start; + align-items: center; +`; + +const MascotImage = styled.img` + width: 180px; // Adjust mascot size + height: auto; + animation: float 12s ease-in-out infinite; + + @keyframes float { + 0% { + transform: translateY(0); // Start position + } + 25% { + transform: translateY(-10px); // Slight upward movement and rotation + } + 50% { + transform: translateY(0); // Float higher + } + 75% { + transform: translateY(-10px); // Slight downward movement and rotation + } + 100% { + transform: translateY(0); // Return to start + } + } +`; + const Landing = () => { const navigate = useNavigate(); @@ -244,14 +258,16 @@ const Landing = () => { ))} {/* Single falling star */} - - Welcome To StoryBudget - Manage your budgets effortlessly. - navigate('/signup')}>Get Started - - - - + +
+ Welcome To StoryBudget + Manage your budgets effortlessly. + navigate('/signup')}>Get Started +
+ + + +
) }; From 105cd951f6d3adc7bf991a7edfb6dc84cb7de9ac Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 22 Aug 2025 20:27:22 +0200 Subject: [PATCH 35/61] Remove redundant code that is not needed --- frontend/src/components/pages/Landing.jsx | 24 ++++++----------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index 03686a796a..f2f9dd2034 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -44,8 +44,9 @@ const NavButtons = styled.div` `; const Button = styled.button` - background-color: ${({ theme }) => theme.colors.pink}; - color: ${({ theme }) => theme.colors.black}; + background-color: ${({ theme, variant }) => variant === 'transparent' ? 'inherit' : theme.colors.pink}; + color: ${({ theme, variant }) => + variant === 'transparent' ? theme.colors.white : theme.colors.black}; padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; font-size: ${({ theme }) => theme.typography.fontSize}; border: none; @@ -63,9 +64,7 @@ const StarsContainer = styled.div` left: 0; width: 100%; height: 100%; - overflow: hidden; pointer-events: none; // Prevent interaction with stars - z-index: 1; // Ensure stars are behind the content `; const Star = styled.div` @@ -204,21 +203,12 @@ const MascotImage = styled.img` animation: float 12s ease-in-out infinite; @keyframes float { - 0% { + 0%, 50%, 100%{ transform: translateY(0); // Start position } - 25% { + 25%, 75% { transform: translateY(-10px); // Slight upward movement and rotation } - 50% { - transform: translateY(0); // Float higher - } - 75% { - transform: translateY(-10px); // Slight downward movement and rotation - } - 100% { - transform: translateY(0); // Return to start - } } `; @@ -240,9 +230,7 @@ const Landing = () => { StoryBudget - + From b0f045e57b1354092ca0020698cc3af4ea1f5519 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 22 Aug 2025 20:41:46 +0200 Subject: [PATCH 36/61] Change section text --- frontend/src/components/pages/Landing.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index f2f9dd2034..dec4b8687e 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -249,7 +249,7 @@ const Landing = () => {
Welcome To StoryBudget - Manage your budgets effortlessly. + Your Finances, Your Story, Your Control. navigate('/signup')}>Get Started
From 70d5ea1dff38d0710d097f964a6aed319c379af5 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Fri, 22 Aug 2025 21:03:44 +0200 Subject: [PATCH 37/61] Prevent invalid props from being passed to DOM --- frontend/src/components/pages/Landing.jsx | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx index dec4b8687e..880c9eea52 100644 --- a/frontend/src/components/pages/Landing.jsx +++ b/frontend/src/components/pages/Landing.jsx @@ -44,15 +44,19 @@ const NavButtons = styled.div` `; const Button = styled.button` - background-color: ${({ theme, variant }) => variant === 'transparent' ? 'inherit' : theme.colors.pink}; - color: ${({ theme, variant }) => - variant === 'transparent' ? theme.colors.white : theme.colors.black}; + background-color: ${({ theme }) => theme.colors.pink}; + color: ${({ theme }) => theme.colors.black}; padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; font-size: ${({ theme }) => theme.typography.fontSize}; border: none; border-radius: ${({ theme }) => theme.spacing(2)}; cursor: pointer; + &.transparent { + background-color: inherit; + color: ${({ theme }) => theme.colors.white}; + } + &:hover { background-color: ${({ theme }) => theme.colors.limeGreenLight}; } @@ -67,7 +71,7 @@ const StarsContainer = styled.div` pointer-events: none; // Prevent interaction with stars `; -const Star = styled.div` +const Star = styled(({ top, left, size, duration, ...rest }) =>
)` position: absolute; width: ${({ size }) => size}px; height: ${({ size }) => size}px; @@ -85,9 +89,9 @@ const Star = styled.div` 39% 35% ); // Creates a star shape background-color: white; - animation: twinkle ${({ duration }) => duration}s infinite ease-in-out; - top: ${({ top }) => top}%; - left: ${({ left }) => left}%; + animation: twinkle ${props => props.duration}s infinite ease-in-out; ease-in-out; + top: ${props => props.top}%; + left: ${props => props.left}%; @keyframes twinkle { 0%, 100% { @@ -230,7 +234,7 @@ const Landing = () => { StoryBudget - + From b7d5b8d6d204b24bf6646128a4fc27aec6cecea9 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sat, 23 Aug 2025 16:05:52 +0200 Subject: [PATCH 38/61] Add SignupContainer, Navbar and styling --- frontend/src/components/pages/Signup.jsx | 145 ++++++++++++++++------- 1 file changed, 103 insertions(+), 42 deletions(-) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 417c380e48..512122672e 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -1,6 +1,59 @@ import { useState } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +const SignupContainer = styled.div` + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + font-family: ${({ theme }) => theme.typography.fontFamily}; + height: 100vh; + display: flex; + flex-direction: column; + position: relative; +`; + +const Navbar = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${({ theme }) => theme.spacing(4)}; + border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; + background-color: ${({ theme }) => theme.colors.primary}; + width: 100%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const Logo = styled.h1` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + color: ${({ theme }) => theme.colors.white}; + margin: 0; +`; + +const NavButtons = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; + + @media (max-width: 767px) { + display: none; + } +`; + +const Button = styled.button` + background-color: ${({ theme }) => theme.colors.pink}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.colors.limeGreenLight}; + } +`; + const Signup = () => { const [name, setName] = useState(''); @@ -37,49 +90,57 @@ const Signup = () => { } return ( -
-

Create Your Account

-
-
- - setName(e.target.value)} - required - /> -
-
- - setEmail(e.target.value)} - required - /> -
-
- - setPassword(e.target.value)} - required - /> + + + StoryBudget + + + + +
+

Create Your Account

+ +
+ + setName(e.target.value)} + required + /> +
+
+ + setEmail(e.target.value)} + required + />
- - {error &&

{error}

} - -

Already have an account? Login

-
+
+ + setPassword(e.target.value)} + required + /> +
+ + {error &&

{error}

} + +

Already have an account? Login

+
+ ); }; From 9d26881ce1b2e35586462232ed1c5f3045f6fd1b Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 24 Aug 2025 16:06:19 +0200 Subject: [PATCH 39/61] Add LeftSection and RightSection ti Signup.jsx with styling and media queries --- frontend/src/components/pages/Signup.jsx | 204 +++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index 512122672e..4f4c4098dc 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import Mascot from '../../assets/images/Mascot.svg'; const SignupContainer = styled.div` background-color: ${({ theme }) => theme.colors.primary}; @@ -54,6 +55,180 @@ const Button = styled.button` } `; +const Content = styled.div` + display: flex; + flex: 1; + height: 100%; +`; + +const LeftSection = styled.div` + flex: 1; + padding: ${({ theme }) => theme.spacing(8)}; + display: flex; + flex-direction: column; + justify-content: center; + background-color: ${({ theme }) => theme.colors.white}; + color: ${({ theme }) => theme.colors.black}; + + h1 { + font-size: 1.5rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; + max-width: 400px; + text-align: left; + margin-left : auto; + margin-right: auto; + + @media (min-width: 768px) { + font-size: 2rem; + } + } + + form { + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(4)}; + max-width: 400px; + width: 100%; + margin: 0 auto; + } + + label { + font-size: 1rem; + margin-bottom: ${({ theme }) => theme.spacing(1)}; + } + + input { + padding: ${({ theme }) => theme.spacing(2)}; + font-size: 1rem; + border: 1px solid ${({ theme }) => theme.colors.greyLight}; + border-radius: ${({ theme }) => theme.spacing(1)}; + width: 100%; + box-sizing: border-box; + } + + button { + padding: ${({ theme }) => theme.spacing(2)}; + font-size: 1rem; + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + margin-top: ${({ theme }) => theme.spacing(4)}; + + &:hover { + background-color: ${({ theme }) => theme.colors.primaryDark}; + } + + &:disabled { + background-color: ${({ theme }) => theme.colors.greyLight}; + cursor: not-allowed; + } + } + + .error-message { + color: ${({ theme }) => theme.colors.error}; + font-size: 0.9rem; + margin-top: ${({ theme }) => theme.spacing(2)}; + } + + p { + margin-top: ${({ theme }) => theme.spacing(4)}; + font-size: 0.9rem; + max-width: 400px; + text-align: left; + margin-left: auto; + margin-right: auto; + + a { + color: ${({ theme }) => theme.colors.primary}; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } +`; + +const RightSection = styled.div` + flex: 1; + padding: ${({ theme }) => theme.spacing(8)}; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + background-color: ${({ theme }) => theme.colors.primary}; + position: relative; + overflow: hidden; + + @media (max-width: 768px) { + display: none; /* Hide the RightSection on smaller screens */ + } +`; + + +const StarsContainer = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; // Prevent interaction with stars +`; + +const Star = styled(({ top, left, size, duration, ...rest }) =>
)` +position: absolute; +width: ${({ size }) => size}px; +height: ${({ size }) => size}px; +background-color: transparent; +clip-path: polygon( + 50% 0%, + 61% 35%, + 98% 35%, + 68% 57%, + 79% 91%, + 50% 70%, + 21% 91%, + 32% 57%, + 2% 35%, + 39% 35% +); // Creates a star shape +background-color: white; +animation: twinkle ${props => props.duration}s infinite ease-in-out; ease-in-out; +top: ${props => props.top}%; +left: ${props => props.left}%; + +@keyframes twinkle { + 0%, 100% { + opacity: 0.2; + } + 50% { + opacity: 1; + } +} +`; + +const Heading = styled.h2` + font-size: 2rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const Paragraph = styled.p` + font-size: 1rem; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const MascotImage = styled.img` + width: 200px; + height: auto; + margin-top: ${({ theme }) => theme.spacing(6)}; +`; + const Signup = () => { const [name, setName] = useState(''); @@ -89,6 +264,15 @@ const Signup = () => { } } + // Generate random stars + const stars = Array.from({ length: 50 }, (_, i) => ({ + id: i, + size: Math.random() * 3 + 2, // Random size between 2px and 5px + duration: Math.random() * 3 + 2, // Random animation duration between 2s and 5s + top: Math.random() * 100, // Random position (top) + left: Math.random() * 100, // Random position (left) + })); + return ( @@ -97,6 +281,8 @@ const Signup = () => { + +

Create Your Account

@@ -140,6 +326,24 @@ const Signup = () => {

Already have an account? Login

+
+ + + {stars.map((star) => ( + + ))} + + Welcome to StoryBudget + Manage your budgets effortlessly and take control of your finances today! + + +
); }; From f1b91e81e3b47d5b03f7ac605b05258a615fe51a Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 24 Aug 2025 19:22:44 +0200 Subject: [PATCH 40/61] Add LeftSection and RightSection to Login.jsx with styling and media queries --- frontend/src/components/pages/Login.jsx | 264 ++++++++++++++++++++++++ 1 file changed, 264 insertions(+) diff --git a/frontend/src/components/pages/Login.jsx b/frontend/src/components/pages/Login.jsx index 0ce550011a..9db790c420 100644 --- a/frontend/src/components/pages/Login.jsx +++ b/frontend/src/components/pages/Login.jsx @@ -1,6 +1,233 @@ import { useState } from 'react'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import Mascot from '../../assets/images/Mascot.svg'; + +const LoginContainer = styled.div` + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + font-family: ${({ theme }) => theme.typography.fontFamily}; + height: 100vh; + display: flex; + flex-direction: column; + position: relative; +`; + +const Navbar = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${({ theme }) => theme.spacing(4)}; + border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; + background-color: ${({ theme }) => theme.colors.primary}; + width: 100%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const Logo = styled.h1` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + color: ${({ theme }) => theme.colors.white}; + margin: 0; +`; + +const NavButtons = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; + + @media (max-width: 767px) { + display: none; + } +`; + +const Button = styled.button` + background-color: ${({ theme }) => theme.colors.pink}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.colors.limeGreenLight}; + } +`; + +const Content = styled.div` + display: flex; + flex: 1; + height: 100%; +`; + +const LeftSection = styled.div` + flex: 1; + padding: ${({ theme }) => theme.spacing(8)}; + display: flex; + flex-direction: column; + justify-content: center; + background-color: ${({ theme }) => theme.colors.white}; + color: ${({ theme }) => theme.colors.black}; + + h1 { + font-size: 1.5rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; + max-width: 400px; + text-align: left; + margin-left : auto; + margin-right: auto; + + @media (min-width: 768px) { + font-size: 2rem; + } + } + + form { + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(4)}; + max-width: 400px; + width: 100%; + margin: 0 auto; + } + + label { + font-size: 1rem; + margin-bottom: ${({ theme }) => theme.spacing(1)}; + } + + input { + padding: ${({ theme }) => theme.spacing(2)}; + font-size: 1rem; + border: 1px solid ${({ theme }) => theme.colors.greyLight}; + border-radius: ${({ theme }) => theme.spacing(1)}; + width: 100%; + box-sizing: border-box; + } + + button { + padding: ${({ theme }) => theme.spacing(2)}; + font-size: 1rem; + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + margin-top: ${({ theme }) => theme.spacing(4)}; + + &:hover { + background-color: ${({ theme }) => theme.colors.primaryDark}; + } + + &:disabled { + background-color: ${({ theme }) => theme.colors.greyLight}; + cursor: not-allowed; + } + } + + .error-message { + color: ${({ theme }) => theme.colors.error}; + font-size: 0.9rem; + margin-top: ${({ theme }) => theme.spacing(2)}; + } + + p { + margin-top: ${({ theme }) => theme.spacing(4)}; + font-size: 0.9rem; + max-width: 400px; + text-align: left; + margin-left: auto; + margin-right: auto; + + a { + color: ${({ theme }) => theme.colors.primary}; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } +`; + +const RightSection = styled.div` + flex: 1; + padding: ${({ theme }) => theme.spacing(8)}; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + background-color: ${({ theme }) => theme.colors.primary}; + position: relative; + overflow: hidden; + + @media (max-width: 768px) { + display: none; /* Hide the RightSection on smaller screens */ + } +`; + + +const StarsContainer = styled.div` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; // Prevent interaction with stars +`; + +const Star = styled(({ top, left, size, duration, ...rest }) =>
)` +position: absolute; +width: ${({ size }) => size}px; +height: ${({ size }) => size}px; +background-color: transparent; +clip-path: polygon( + 50% 0%, + 61% 35%, + 98% 35%, + 68% 57%, + 79% 91%, + 50% 70%, + 21% 91%, + 32% 57%, + 2% 35%, + 39% 35% +); // Creates a star shape +background-color: white; +animation: twinkle ${props => props.duration}s infinite ease-in-out; ease-in-out; +top: ${props => props.top}%; +left: ${props => props.left}%; + +@keyframes twinkle { + 0%, 100% { + opacity: 0.2; + } + 50% { + opacity: 1; + } +} +`; + +const Heading = styled.h2` + font-size: 2rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const Paragraph = styled.p` + font-size: 1rem; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const MascotImage = styled.img` + width: 200px; + height: auto; + margin-top: ${({ theme }) => theme.spacing(6)}; +`; const Login = () => { const [email, setEmail] = useState(''); @@ -34,7 +261,25 @@ const Login = () => { } } + // Generate random stars + const stars = Array.from({ length: 50 }, (_, i) => ({ + id: i, + size: Math.random() * 3 + 2, // Random size between 2px and 5px + duration: Math.random() * 3 + 2, // Random animation duration between 2s and 5s + top: Math.random() * 100, // Random position (top) + left: Math.random() * 100, // Random position (left) + })); + return ( + + + StoryBudget + + + + + +

Login

@@ -67,6 +312,25 @@ const Login = () => {

Don't have an account? Sign up

+
+ + + {stars.map((star) => ( + + ))} + + Welcome to StoryBudget + Manage your budgets effortlessly and take control of your finances today! + + +
+
); }; From 96f7b582b0ea5c59a409312c20780042c515f086 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 24 Aug 2025 19:33:25 +0200 Subject: [PATCH 41/61] Add autocomplete to Signup.jsx and Login.jsx to remove warnings and improve user experience --- frontend/src/components/pages/Login.jsx | 2 ++ frontend/src/components/pages/Signup.jsx | 3 +++ 2 files changed, 5 insertions(+) diff --git a/frontend/src/components/pages/Login.jsx b/frontend/src/components/pages/Login.jsx index 9db790c420..c89a90a4c2 100644 --- a/frontend/src/components/pages/Login.jsx +++ b/frontend/src/components/pages/Login.jsx @@ -292,6 +292,7 @@ const Login = () => { value={email} onChange={(e) => setEmail(e.target.value)} required + autoComplete='email' />
@@ -303,6 +304,7 @@ const Login = () => { value={password} onChange={(e) => setPassword(e.target.value)} required + autoComplete='current-password' />
@@ -306,6 +307,7 @@ const Signup = () => { value={email} onChange={(e) => setEmail(e.target.value)} required + autoComplete='email' />
@@ -317,6 +319,7 @@ const Signup = () => { value={password} onChange={(e) => setPassword(e.target.value)} required + autoComplete='new-password' />
+ + + + ); }; export default Dashboard; \ No newline at end of file From 7454c3a524c5bd9835fde77763341502ae15de27 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 25 Aug 2025 13:25:29 +0200 Subject: [PATCH 44/61] Install react icons. Add DashboardContainer, LeftPanel, RightContent and react icons to Dashboard.jsx --- frontend/package.json | 1 + frontend/src/components/pages/Dashboard.jsx | 179 ++++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 0d590a8d9b..632fbca504 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "axios": "^1.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.8.0", "styled-components": "^6.1.19", "zustand": "^5.0.7" diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index ba1c2e3817..98e4832d8d 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -1,5 +1,7 @@ import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import { FiBarChart2, FiCreditCard, FiTrendingUp, FiEdit } from 'react-icons/fi'; +import { FaCoins } from 'react-icons/fa'; const Navbar = styled.nav` display: flex; @@ -42,6 +44,116 @@ const Button = styled.button` } `; +const DashboardContainer = styled.div` + display: flex; + height: calc(100vh - ${({ theme }) => theme.spacing(8)}); // Full height minus navbar height +`; + +const LeftPanel = styled.div` + width: 190px; + background-color: ${({ theme }) => theme.colors.white}; + padding: ${({ theme }) => theme.spacing(4)}; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(1)}; + + h2 { + margin-top: ${({ theme }) => theme.spacing(4)}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const TabButton = styled.button` + background-color: ${({ theme }) => theme.colors.greyLight}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(3)}; + font-size: 1rem; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + text-align: left; + width: 100%; + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; + + &:hover { + background-color: ${({ theme }) => theme.colors.primaryLight}; + color: ${({ theme }) => theme.colors.white}; + } +`; + +const RightContent = styled.div` + flex: 1; + padding: ${({ theme }) => theme.spacing(4)}; + display: grid; + gap: ${({ theme }) => theme.spacing(4)}; + grid-template-columns: repeat(1, 1fr); // Default to one column + grid-auto-rows: 1fr; // Ensure all rows have equal height + + @media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); // Two columns on tablet + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(2, 1fr); // Two columns on desktop + } +`; + +const Section = styled.div` + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.greyLight}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const CardContainer = styled.div` + display: grid; + gap: ${({ theme }) => theme.spacing(4)}; + grid-template-columns: repeat(1, 1fr); // Default to one column + + @media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); // Two columns on tablet + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(2, 1fr); // Two columns on desktop + } +`; + +const Card = styled.div` + background-color: ${({ theme }) => theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; + position: relative; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + h4 { + font-size: 1.25rem; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + } + + p { + font-size: 1rem; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + } + + span { + font-size: 0.9rem; + color: ${({ theme }) => theme.colors.primaryDark}; + } + + .edit-icon { + position: absolute; + top: ${({ theme }) => theme.spacing(2)}; + right: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + font-size: 1rem; // Adjust the size of the emoji + } +`; + + const Dashboard = () => { const navigate = useNavigate(); const handleLogout = () => { @@ -57,6 +169,73 @@ const Dashboard = () => { + + +

Dashboard

+ + Balance + + + Spendings + + + Savings + + + Income + +
+ +
+ + + + + +

Balance

+

SEK 0

+ Additional Info +
+ + + + +

Spendings

+

SEK 0

+ Additional Info +
+ + + + +

Savings

+

SEK 0

+ Additional Info +
+ + navigate('/income-management')} > + + +

Income

+

SEK 0

+ Additional Info +
+
+
+
+

Section 2

+

Content for section 2.

+
+
+

Section 3

+

Content for section 3.

+
+
+

Section 4

+

Content for section 4.

+
+
+
); }; From 1e5d70c934881d4e714f8762755b148f3df78e30 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 25 Aug 2025 16:57:11 +0200 Subject: [PATCH 45/61] Add income management functionality for maninging income data --- backend/routes/incomeRoutes.js | 31 ++++ backend/server.js | 2 + frontend/src/App.jsx | 2 + frontend/src/components/pages/Dashboard.jsx | 47 +++++- .../src/components/pages/IncomeManagement.jsx | 138 ++++++++++++++++++ 5 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 backend/routes/incomeRoutes.js create mode 100644 frontend/src/components/pages/IncomeManagement.jsx diff --git a/backend/routes/incomeRoutes.js b/backend/routes/incomeRoutes.js new file mode 100644 index 0000000000..3bd1e4d489 --- /dev/null +++ b/backend/routes/incomeRoutes.js @@ -0,0 +1,31 @@ +import express from "express"; + +const router = express.Router(); + +// In-memory storage for income (replace with a database in production) +let incomeData = { income: 0 }; + +// GET /income - Fetch the current income +router.get("/", (req, res) => { + res.status(200).json(incomeData); +}); + +// POST /income - Add or update the income +router.post("/", (req, res) => { + const { income } = req.body; + + if (typeof income !== "number" || income < 0) { + return res.status(400).json({ error: "Invalid income value" }); + } + + incomeData.income = income; + res.status(200).json({ message: "Income updated successfully", income: incomeData.income }); +}); + +// DELETE /income - Delete the income +router.delete("/", (req, res) => { + incomeData = { income: 0 }; + res.status(200).json({ message: "Income deleted successfully" }); +}); + +export default router; // Ensure this line is present \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 849b9e16a0..ea48df2756 100644 --- a/backend/server.js +++ b/backend/server.js @@ -3,6 +3,7 @@ import cors from "cors"; import mongoose from "mongoose"; import dotenv from "dotenv"; import userRoutes from "./routes/userRoutes.js"; +import incomeRoutes from "./routes/incomeRoutes.js"; import listEndpoints from "express-list-endpoints"; dotenv.config(); @@ -29,6 +30,7 @@ app.get("/", (req, res) => { //endpoint routes app.use('/users', userRoutes); +app.use('/income', incomeRoutes); // Start the server app.listen(port, () => { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 35f3df9e75..a486579ae0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import Landing from './components/pages/Landing'; import Signup from './components/pages/Signup'; import Login from './components/pages/Login'; import Dashboard from './components/pages/Dashboard'; +import IncomeManagement from './components/pages/IncomeManagement'; export const App = () => { @@ -17,6 +18,7 @@ export const App = () => { } /> } /> } /> + } /> ); diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index 98e4832d8d..4b985195df 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -1,3 +1,4 @@ +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { FiBarChart2, FiCreditCard, FiTrendingUp, FiEdit } from 'react-icons/fi'; @@ -60,6 +61,7 @@ const LeftPanel = styled.div` h2 { margin-top: ${({ theme }) => theme.spacing(4)}; margin-bottom: ${({ theme }) => theme.spacing(2)}; + } `; const TabButton = styled.button` @@ -153,14 +155,47 @@ const Card = styled.div` } `; - const Dashboard = () => { + const [income, setIncome] = useState(0); + const [loading, setLoading] = useState(true); const navigate = useNavigate(); + const handleLogout = () => { - localStorage.removeItem('accessToken'); // Example: Clear the access token - navigate('/'); // Redirect to the landing page + localStorage.removeItem('accessToken'); + navigate('/'); }; + useEffect(() => { + const fetchIncome = async () => { + try { + setLoading(true); + const incomeData = await fetchIncomeFromAPI(); // Replace with your API call + setIncome(incomeData); // Update the state with fetched income + } catch (error) { + console.error('Error fetching income:', error); + alert('Failed to fetch income data. Please try again later.'); + } finally { + setLoading(false); + } + }; + + fetchIncome(); + }, []); // Run this effect only once when the component mounts + + const fetchIncomeFromAPI = async () => { + try { + const response = await fetch('http://localhost:8080/income'); // Replace with your actual API endpoint + if (!response.ok) { + throw new Error('Failed to fetch income'); + } + const data = await response.json(); + return data.income; // Assuming the API returns { income: 29000 } + } catch (error) { + console.error('Error fetching income:', error); + return 0; // Default to 0 if there's an error + } + }; + return ( <> @@ -217,7 +252,11 @@ const Dashboard = () => {

Income

-

SEK 0

+ {loading ? ( +

Loading...

+ ) : ( +

SEK {income}

+ )} Additional Info diff --git a/frontend/src/components/pages/IncomeManagement.jsx b/frontend/src/components/pages/IncomeManagement.jsx new file mode 100644 index 0000000000..db571bae6b --- /dev/null +++ b/frontend/src/components/pages/IncomeManagement.jsx @@ -0,0 +1,138 @@ + +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +const Navbar = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${({ theme }) => theme.spacing(4)}; + border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; + background-color: ${({ theme }) => theme.colors.primary}; + width: 100%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const Logo = styled.h1` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + color: ${({ theme }) => theme.colors.white}; + margin: 0; +`; + +const NavButtons = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; + + @media (max-width: 767px) { + display: none; + } +`; + + +const Button = styled.button` + background-color: ${({ theme }) => theme.colors.pink}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.colors.limeGreenLight}; + } +`; + +const IncomeManagementContainer = styled.div` + padding: 2rem; +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +const Input = styled.input` + padding: 0.5rem; + font-size: 1rem; + border: 1px solid #ccc; + border-radius: 4px; +`; + +const SaveButton = styled(Button)` + background-color: ${({ theme }) => theme.colors.primary}; + color: white; + + &:hover { + background-color: ${({ theme }) => theme.colors.primaryDark}; + } +`; + +const IncomeManagement = () => { + const navigate = useNavigate(); + const [income, setIncome] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (income <= 0) { + alert('Please enter a valid income value greater than 0.'); + return; + } + + try { + await saveIncomeToAPI(Number(income)); + console.log('Income submitted:', income); + + navigate('/dashboard'); // Navigate back to the dashboard after submission + } catch (error) { + console.error('Error submitting income:', error); + alert('Failed to save income. Please try again.'); + } + }; + + const saveIncomeToAPI = async (income) => { + const response = await fetch('http://localhost:8080/income', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ income }), + }); + + if (!response.ok) { + throw new Error('Failed to save income'); + } + }; + + return ( + <> + + StoryBudget + + + + + +

Manage Income

+
+ + setIncome(e.target.value)} + placeholder="Enter your income after tax" + required + /> + Save Income +
+
+ + ); +}; + +export default IncomeManagement; \ No newline at end of file From d7a0ff2fa768b852b848e55b2098324e854ce2e7 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 25 Aug 2025 17:26:22 +0200 Subject: [PATCH 46/61] Persist income data using MongoDB to ensure income data persists restart and page reload --- backend/models/Income.js | 12 +++++++++ backend/routes/incomeRoutes.js | 47 +++++++++++++++++++++++++--------- backend/server.js | 5 +++- 3 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 backend/models/Income.js diff --git a/backend/models/Income.js b/backend/models/Income.js new file mode 100644 index 0000000000..1a1f448236 --- /dev/null +++ b/backend/models/Income.js @@ -0,0 +1,12 @@ +import mongoose from 'mongoose' + +const incomeSchema = new mongoose.Schema({ + amount: { + type: Number, + required: true, + default: 0, + }, +}); + + +export const Income = mongoose.model("Income", incomeSchema); \ No newline at end of file diff --git a/backend/routes/incomeRoutes.js b/backend/routes/incomeRoutes.js index 3bd1e4d489..993e0fcad3 100644 --- a/backend/routes/incomeRoutes.js +++ b/backend/routes/incomeRoutes.js @@ -1,31 +1,54 @@ import express from "express"; +import { Income } from "../models/Income.js"; const router = express.Router(); -// In-memory storage for income (replace with a database in production) -let incomeData = { income: 0 }; - // GET /income - Fetch the current income -router.get("/", (req, res) => { - res.status(200).json(incomeData); +router.get("/", async (req, res) => { + try { + let income = await Income.findOne(); + if (!income) { + income = await Income.create({ amount: 0 }); + } + res.status(200).json({ income: income.amount }); + } catch (error) { + console.error("Error fetching income:", error); + res.status(500).json({ error: "Server error" }); + } }); // POST /income - Add or update the income -router.post("/", (req, res) => { +router.post("/", async (req, res) => { const { income } = req.body; if (typeof income !== "number" || income < 0) { return res.status(400).json({ error: "Invalid income value" }); } - incomeData.income = income; - res.status(200).json({ message: "Income updated successfully", income: incomeData.income }); + try { + let incomeData = await Income.findOne(); + if (!incomeData) { + incomeData = await Income.create({ amount: income }); + } else { + incomeData.amount = income; + await incomeData.save(); + } + res.status(200).json({ message: "Income updated successfully", income: incomeData.amount }); + } catch (error) { + console.error("Error updating income:", error); + res.status(500).json({ error: "Server error" }); + } }); // DELETE /income - Delete the income -router.delete("/", (req, res) => { - incomeData = { income: 0 }; - res.status(200).json({ message: "Income deleted successfully" }); +router.delete("/", async (req, res) => { + try { + await Income.deleteMany(); // Remove all income records + res.status(200).json({ message: "Income deleted successfully" }); + } catch (error) { + console.error("Error deleting income:", error); + return res.status(500).json({ error: "Server error" }); + } }); -export default router; // Ensure this line is present \ No newline at end of file +export default router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index ea48df2756..cd293a37a5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,7 +10,10 @@ dotenv.config(); console.log('JWT_SECRET:', process.env.JWT_SECRET); const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; -mongoose.connect(mongoUrl); +mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, +}); mongoose.Promise = Promise; const port = process.env.PORT || 8080; From 07156607bdf3a1fa1107816b7a2b8c1f520f709c Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Mon, 25 Aug 2025 17:36:08 +0200 Subject: [PATCH 47/61] Improve layout and responsiveness ot IncomeManagement page --- .../src/components/pages/IncomeManagement.jsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/pages/IncomeManagement.jsx b/frontend/src/components/pages/IncomeManagement.jsx index db571bae6b..e319ffb694 100644 --- a/frontend/src/components/pages/IncomeManagement.jsx +++ b/frontend/src/components/pages/IncomeManagement.jsx @@ -30,7 +30,6 @@ const NavButtons = styled.div` } `; - const Button = styled.button` background-color: ${({ theme }) => theme.colors.pink}; color: ${({ theme }) => theme.colors.black}; @@ -46,25 +45,31 @@ const Button = styled.button` `; const IncomeManagementContainer = styled.div` - padding: 2rem; + max-width: 400px; /* Set a max-width for the content */ + margin: 40px auto; /* Center the container */ + padding: ${({ theme }) => theme.spacing(4)}; + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.greyLight}; + border-radius: ${({ theme }) => theme.spacing(2)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); `; const Form = styled.form` display: flex; flex-direction: column; - gap: 1rem; + gap: ${({ theme }) => theme.spacing(4)}; `; const Input = styled.input` - padding: 0.5rem; - font-size: 1rem; - border: 1px solid #ccc; - border-radius: 4px; + padding: ${({ theme }) => theme.spacing(2)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; `; const SaveButton = styled(Button)` background-color: ${({ theme }) => theme.colors.primary}; - color: white; + color: ${({ theme }) => theme.colors.white}; &:hover { background-color: ${({ theme }) => theme.colors.primaryDark}; From d8520fa0d2d04c4061b560926871e4c25a072c01 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Tue, 26 Aug 2025 13:27:48 +0200 Subject: [PATCH 48/61] Install recharts and add staple diagram and styling to Dashboard.jsx --- frontend/package.json | 1 + frontend/src/components/pages/Dashboard.jsx | 51 ++++++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 632fbca504..ae37bdda4f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-router-dom": "^7.8.0", + "recharts": "^3.1.2", "styled-components": "^6.1.19", "zustand": "^5.0.7" }, diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index 4b985195df..9cf4ff17ec 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { FiBarChart2, FiCreditCard, FiTrendingUp, FiEdit } from 'react-icons/fi'; import { FaCoins } from 'react-icons/fa'; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { theme } from '../../styles/theme'; const Navbar = styled.nav` display: flex; @@ -90,10 +92,9 @@ const RightContent = styled.div` display: grid; gap: ${({ theme }) => theme.spacing(4)}; grid-template-columns: repeat(1, 1fr); // Default to one column - grid-auto-rows: 1fr; // Ensure all rows have equal height - @media (min-width: 768px) { - grid-template-columns: repeat(2, 1fr); // Two columns on tablet + @media (min-width: 768px) and (max-width: 1023px) { + grid-template-columns: repeat(1, 1fr); // Two columns on tablet } @media (min-width: 1024px) { @@ -107,6 +108,16 @@ const Section = styled.div` border-radius: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(4)}; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + max-height: 75%; + + &.chart-section { + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + } + + &.chart-section h3 { + margin-bottom: ${({ theme }) => theme.spacing(2)}; + } `; const CardContainer = styled.div` @@ -155,6 +166,16 @@ const Card = styled.div` } `; +// Sample data for the bar chart +const data = [ + { name: 'Jan', Income: 25000, Spendings: 18500 }, + { name: 'Feb', Income: 25000, Spendings: 19300 }, + { name: 'Mar', Income: 25000, Spendings: 19800 }, + { name: 'Apr', Income: 25000, Spendings: 19000 }, + { name: 'May', Income: 25000, Spendings: 19800 }, + { name: 'Jun', Income: 25000, Spendings: 20800 }, +]; + const Dashboard = () => { const [income, setIncome] = useState(0); const [loading, setLoading] = useState(true); @@ -223,7 +244,7 @@ const Dashboard = () => {
- + @@ -252,18 +273,24 @@ const Dashboard = () => {

Income

- {loading ? ( -

Loading...

- ) : ( -

SEK {income}

- )} + {loading ? null :

SEK {income}

} Additional Info
-
-

Section 2

-

Content for section 2.

+
+

Balance

+ + + + + + + + + + +

Section 3

From 92fa263ac0475db39342fc502a79302430ca6441 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Tue, 26 Aug 2025 18:43:38 +0200 Subject: [PATCH 49/61] Remove deprecated MongoDB connection useNewUrlParser and useUnifiedTopology --- backend/server.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/server.js b/backend/server.js index cd293a37a5..ea48df2756 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,10 +10,7 @@ dotenv.config(); console.log('JWT_SECRET:', process.env.JWT_SECRET); const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project"; -mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, -}); +mongoose.connect(mongoUrl); mongoose.Promise = Promise; const port = process.env.PORT || 8080; From a32ebfae704ab754e38eba06d944008d22a62f69 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 27 Aug 2025 13:34:12 +0200 Subject: [PATCH 50/61] Add spendings Manamgent functionality --- backend/models/Spendings.js | 21 +++ backend/routes/spendingsRoutes.js | 42 +++++ backend/server.js | 4 +- frontend/src/App.jsx | 2 + frontend/src/components/pages/Dashboard.jsx | 58 ++++--- .../components/pages/SpendingsManagement.jsx | 160 ++++++++++++++++++ 6 files changed, 261 insertions(+), 26 deletions(-) create mode 100644 backend/models/Spendings.js create mode 100644 backend/routes/spendingsRoutes.js create mode 100644 frontend/src/components/pages/SpendingsManagement.jsx diff --git a/backend/models/Spendings.js b/backend/models/Spendings.js new file mode 100644 index 0000000000..50cfab3bfd --- /dev/null +++ b/backend/models/Spendings.js @@ -0,0 +1,21 @@ +import mongoose from 'mongoose' + +const spendingsSchema = new mongoose.Schema({ + category: { + type: String, + required: true, + enum: ["grocery", "shopping", "entertainment", "restaurants", "cafe", "travel", "others"] +}, + amount: { + type: Number, + required: true, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + + +export const Spendings = mongoose.model("Spendings", spendingsSchema); \ No newline at end of file diff --git a/backend/routes/spendingsRoutes.js b/backend/routes/spendingsRoutes.js new file mode 100644 index 0000000000..1ddedc7fbc --- /dev/null +++ b/backend/routes/spendingsRoutes.js @@ -0,0 +1,42 @@ +import express from "express"; +import { Spendings } from "../models/Spendings.js"; + +const router = express.Router(); + +// POST - Add income +router.post("/", async (req, res) => { + const { category, amount } = req.body; + + try { + let newSpending = await Spendings.create({ category, amount}); + res.status(201).json(newSpending); + } catch (error) { + res.status(400).json({ message: "Failed to add spending", error: error.message }); + } +}); + +// GET - Get all spendings +router.get("/", async (req, res) => { + try { + const spendings = await Spendings.find(); + res.status(200).json(spendings); + } catch (error) { + res.status(500).json({ message: "Failed to fetch spendings", error: error.message }); + } +}); + +// GET - Get total spendings +router.get("/total", async (req, res) => { + try { + const total = await Spendings.aggregate([ + { $group: {_id: null, totalAmount: { $sum: "$amount" } } }, + ]); + console.log("Total Spendings Calculation:", total); + res.status(200).json({ total: total[0]?.totalAmount || 0 }); + } catch (error) { + console.error("Error Fetching Total Spendings:", error.message); + res.status(500).json({ message: "Failed to fetch total spendings", error: error.message }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index ea48df2756..6fe0cc021c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,6 +5,7 @@ import dotenv from "dotenv"; import userRoutes from "./routes/userRoutes.js"; import incomeRoutes from "./routes/incomeRoutes.js"; import listEndpoints from "express-list-endpoints"; +import spendingsRoutes from "./routes/spendingsRoutes.js"; dotenv.config(); console.log('JWT_SECRET:', process.env.JWT_SECRET); @@ -30,7 +31,8 @@ app.get("/", (req, res) => { //endpoint routes app.use('/users', userRoutes); -app.use('/income', incomeRoutes); +app.use('/income', incomeRoutes); +app.use('/spendings', spendingsRoutes); // Start the server app.listen(port, () => { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a486579ae0..b98811add0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import Signup from './components/pages/Signup'; import Login from './components/pages/Login'; import Dashboard from './components/pages/Dashboard'; import IncomeManagement from './components/pages/IncomeManagement'; +import SpendingsManagement from './components/pages/SpendingsManagement'; export const App = () => { @@ -19,6 +20,7 @@ export const App = () => { } /> } /> } /> + } /> ); diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index 9cf4ff17ec..3fbfdab882 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -178,6 +178,7 @@ const data = [ const Dashboard = () => { const [income, setIncome] = useState(0); + const [totalSpendings, setTotalSpendings] = useState(0); const [loading, setLoading] = useState(true); const navigate = useNavigate(); @@ -186,34 +187,41 @@ const Dashboard = () => { navigate('/'); }; - useEffect(() => { - const fetchIncome = async () => { - try { - setLoading(true); - const incomeData = await fetchIncomeFromAPI(); // Replace with your API call - setIncome(incomeData); // Update the state with fetched income - } catch (error) { - console.error('Error fetching income:', error); - alert('Failed to fetch income data. Please try again later.'); - } finally { - setLoading(false); - } - }; - - fetchIncome(); - }, []); // Run this effect only once when the component mounts - - const fetchIncomeFromAPI = async () => { + useEffect(() => { + const fetchData = async () => { + setLoading(true); + await fetchIncome(); + await fetchTotalSpendings(); + setLoading(false); + }; + + fetchData(); + }, []); + + + const fetchIncome = async () => { + try { + const response = await fetch('http://localhost:8080/income'); + if (!response.ok) { + throw new Error('Failed to fetch income'); + } + const data = await response.json(); + setIncome(data.income); + } catch (error) { + console.error('Error fetching income:', error); + } + }; + + const fetchTotalSpendings = async () => { try { - const response = await fetch('http://localhost:8080/income'); // Replace with your actual API endpoint + const response = await fetch('http://localhost:8080/spendings/total'); if (!response.ok) { - throw new Error('Failed to fetch income'); + throw new Error('Failed to fetch total spendings'); } const data = await response.json(); - return data.income; // Assuming the API returns { income: 29000 } + setTotalSpendings(data.total); } catch (error) { - console.error('Error fetching income:', error); - return 0; // Default to 0 if there's an error + console.error('Error fetching total spendings:', error); } }; @@ -253,11 +261,11 @@ const Dashboard = () => { Additional Info - + navigate('/spendings-management')} >

Spendings

-

SEK 0

+

SEK {loading ? 'Loading...' : totalSpendings}

Additional Info
diff --git a/frontend/src/components/pages/SpendingsManagement.jsx b/frontend/src/components/pages/SpendingsManagement.jsx new file mode 100644 index 0000000000..5cf2bfcb63 --- /dev/null +++ b/frontend/src/components/pages/SpendingsManagement.jsx @@ -0,0 +1,160 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +const Navbar = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${({ theme }) => theme.spacing(4)}; + border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; + background-color: ${({ theme }) => theme.colors.primary}; + width: 100%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const Logo = styled.h1` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + color: ${({ theme }) => theme.colors.white}; + margin: 0; +`; + +const NavButtons = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; + + @media (max-width: 767px) { + display: none; + } +`; + +const Button = styled.button` + background-color: ${({ theme }) => theme.colors.pink}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.colors.limeGreenLight}; + } +`; + +const IncomeManagementContainer = styled.div` + max-width: 400px; /* Set a max-width for the content */ + margin: 40px auto; /* Center the container */ + padding: ${({ theme }) => theme.spacing(4)}; + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.greyLight}; + border-radius: ${({ theme }) => theme.spacing(2)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const Form = styled.form` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(4)}; +`; + +const Input = styled.input` + padding: ${({ theme }) => theme.spacing(2)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; +`; + +const SaveButton = styled(Button)` + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.white}; + + &:hover { + background-color: ${({ theme }) => theme.colors.primaryDark}; + } +`; + +const SpendingsManagement = () => { + const navigate = useNavigate(); + const [category, setCategory] = useState(''); + const [amount, setAmount] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (amount <= 0) { + alert('Please enter a valid amount greater than 0.'); + return; + } + + try { + await saveSpendingToAPI(category, Number(amount)); + console.log('Spending submitted:', { category, amount }); + + navigate('/dashboard'); // Navigate back to the dashboard after submission + } catch (error) { + console.error('Error submitting spedning:', error); + alert('Failed to save spending. Please try again.'); + } + }; + + const saveSpendingToAPI = async (category, amount) => { + const response = await fetch('http://localhost:8080/spendings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ category, amount + }), + }); + + if (!response.ok) { + throw new Error('Failed to save spending'); + } + }; + + return ( + <> + + StoryBudget + + + + + +

Manage Spendings

+
+ + + + setAmount(e.target.value)} + placeholder="Enter your the amount" + required + /> + Save Spending +
+
+ + ); +}; + +export default SpendingsManagement; \ No newline at end of file From 4cbfbcc37cd2465c72d935c0490b1631529ef747 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Wed, 27 Aug 2025 17:30:26 +0200 Subject: [PATCH 51/61] Enhance SpendingsManagement functionality and UI --- backend/models/Spendings.js | 2 +- .../components/pages/SpendingsManagement.jsx | 119 +++++++++++++++--- 2 files changed, 103 insertions(+), 18 deletions(-) diff --git a/backend/models/Spendings.js b/backend/models/Spendings.js index 50cfab3bfd..a2f1528aa8 100644 --- a/backend/models/Spendings.js +++ b/backend/models/Spendings.js @@ -4,7 +4,7 @@ const spendingsSchema = new mongoose.Schema({ category: { type: String, required: true, - enum: ["grocery", "shopping", "entertainment", "restaurants", "cafe", "travel", "others"] + enum: ["Rent", "Insurance", "Broadband", "Phone", "Grocery", "Shopping", "Entertainment", "Restaurants", "Cafe", "Travel", "Others"] }, amount: { type: Number, diff --git a/frontend/src/components/pages/SpendingsManagement.jsx b/frontend/src/components/pages/SpendingsManagement.jsx index 5cf2bfcb63..17f79fe782 100644 --- a/frontend/src/components/pages/SpendingsManagement.jsx +++ b/frontend/src/components/pages/SpendingsManagement.jsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import { FiX } from 'react-icons/fi'; const Navbar = styled.nav` display: flex; @@ -43,7 +44,7 @@ const Button = styled.button` } `; -const IncomeManagementContainer = styled.div` +const SpendingsManagementContainer = styled.div` max-width: 400px; /* Set a max-width for the content */ margin: 40px auto; /* Center the container */ padding: ${({ theme }) => theme.spacing(4)}; @@ -75,10 +76,73 @@ const SaveButton = styled(Button)` } `; +const CardContainer = styled.div` + display: grid; + gap: ${({ theme }) => theme.spacing(4)}; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + margin-top: ${({ theme }) => theme.spacing(4)}; +`; + +const Card = styled.div` + background-color: ${({ theme }) => theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: relative; + + h4 { + margin-bottom: ${({ theme }) => theme.spacing(2)}; + } + + p { + margin: 0; + + .actions { + margin-top: ${({ theme }) => theme.spacing(2)}; + display: flex; + justify-content: space-between; +} +`; + +const DeleteIcon = styled.button` + position: absolute; + top: ${({ theme }) => theme.spacing(2)}; + right: ${({ theme }) => theme.spacing(2)}; + background: none; + border: none; + color: ${({ theme }) => theme.colors.black}; + font-size: 1rem; + cursor: pointer; +`; + +// Helper function to capitalize the first letter of a string +const capitalizeFirstLetter = (string) => { + return string.charAt(0).toUpperCase() + string.slice(1); +}; + const SpendingsManagement = () => { const navigate = useNavigate(); const [category, setCategory] = useState(''); const [amount, setAmount] = useState(''); + const [spendings, setSpendings] = useState([]); + + useEffect(() => { + const fetchSpendings = async () => { + try { + const response = await fetch('http://localhost:8080/spendings'); + if (!response.ok) { + throw new Error('Failed to fetch spendings'); + } + const data = await response.json(); + setSpendings(data); + } catch (error) { + console.error('Error fetching spendings:', error); + } + }; + + fetchSpendings(); + }, []); const handleSubmit = async (e) => { e.preventDefault(); @@ -88,11 +152,13 @@ const SpendingsManagement = () => { return; } - try { - await saveSpendingToAPI(category, Number(amount)); - console.log('Spending submitted:', { category, amount }); - - navigate('/dashboard'); // Navigate back to the dashboard after submission + try { + const newSpending = await saveSpendingToAPI(category, Number(amount)); + console.log('Spending submitted:', newSpending); + + setSpendings(prevSpendings => [...prevSpendings, newSpending]); + setCategory(''); + setAmount(''); } catch (error) { console.error('Error submitting spedning:', error); alert('Failed to save spending. Please try again.'); @@ -112,6 +178,9 @@ const SpendingsManagement = () => { if (!response.ok) { throw new Error('Failed to save spending'); } + + const newSpending = await response.json(); // Parse the response to get the new spending + return newSpending; }; return ( @@ -122,7 +191,7 @@ const SpendingsManagement = () => { - +

Manage Spendings

@@ -133,13 +202,17 @@ const SpendingsManagement = () => { required > - - - - - - - + + + + + + + + + + + { placeholder="Enter your the amount" required /> - Save Spending + Add Spending
-
+ + {spendings.map((spending) => ( + + + + +

{capitalizeFirstLetter(spending.category)}

+

Amount: SEK {spending.amount}

+

Date: {new Date(spending.createdAt).toLocaleDateString()}

+
+ ))} +
+ ); }; From 749cf8a934da9b05264872d4a6357d906592f6a5 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Thu, 28 Aug 2025 11:43:02 +0200 Subject: [PATCH 52/61] Add VariableExpenses component to display expense categories and styling --- frontend/src/components/pages/Dashboard.jsx | 20 +-- .../src/components/pages/VariableExpenses.jsx | 118 ++++++++++++++++++ 2 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/pages/VariableExpenses.jsx diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index 3fbfdab882..b93c3eba5f 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -5,6 +5,7 @@ import { FiBarChart2, FiCreditCard, FiTrendingUp, FiEdit } from 'react-icons/fi' import { FaCoins } from 'react-icons/fa'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { theme } from '../../styles/theme'; +import VariableExpenses from './VariableExpenses'; const Navbar = styled.nav` display: flex; @@ -108,7 +109,10 @@ const Section = styled.div` border-radius: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(4)}; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - max-height: 75%; + + h2 { + margin-bottom: ${({ theme }) => theme.spacing(2)}; + } &.chart-section { background-color: ${({ theme }) => theme.colors.primary}; @@ -135,7 +139,7 @@ const CardContainer = styled.div` `; const Card = styled.div` - background-color: ${({ theme }) => theme.colors.greyLight}; + background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; border: 1px solid ${({ theme }) => theme.colors.grey}; border-radius: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(4)}; @@ -252,7 +256,7 @@ const Dashboard = () => {
- + @@ -260,7 +264,7 @@ const Dashboard = () => {

SEK 0

Additional Info
- + navigate('/spendings-management')} > @@ -268,7 +272,7 @@ const Dashboard = () => {

SEK {loading ? 'Loading...' : totalSpendings}

Additional Info
- + @@ -276,7 +280,7 @@ const Dashboard = () => {

SEK 0

Additional Info
- + navigate('/income-management')} > @@ -301,8 +305,8 @@ const Dashboard = () => {
-

Section 3

-

Content for section 3.

+

Varable Expenses

+

Section 4

diff --git a/frontend/src/components/pages/VariableExpenses.jsx b/frontend/src/components/pages/VariableExpenses.jsx new file mode 100644 index 0000000000..d10c75f9ff --- /dev/null +++ b/frontend/src/components/pages/VariableExpenses.jsx @@ -0,0 +1,118 @@ +import React from 'react'; +import styled from 'styled-components'; +import { theme } from '../../styles/theme'; +import { FiHome } from 'react-icons/fi'; +import { MdOutlineMovie } from 'react-icons/md'; +import { AiOutlineShoppingCart, AiOutlineSafetyCertificate } from 'react-icons/ai'; +import { BiMobileAlt } from 'react-icons/bi'; + + +const VariableExpensesContainer = styled.div` + display: flex; + flex-direction: column; + grid-template-columns: repeat(2, 1fr); + gap: ${({ theme }) => theme.spacing(1)}; +`; + +const QuadrantCardPair = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const Quadrant = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + span { + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + } +`; + +const Card = styled.div` + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: ${({ theme }) => theme.spacing(2)}; + background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + height: 48px; + + h4 { + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightSemiBold}; + } + + p { + font-size: 1rem; + } +`; + +const VariableExpenses = () => { + return ( + <> + + + + + + +

Rent

+

SEK 0

+
+
+ + + + + +

Insurance

+

SEK 0

+
+
+ + + + + +

Grocery

+

SEK 0

+
+
+ + + + + +

Entertainment

+

SEK 0

+
+
+ + + + + +

Phone

+

SEK 0

+
+
+
+ + ); +}; + +export default VariableExpenses; \ No newline at end of file From d0f75cca201c07119040d3aec8c469334b291616 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sat, 30 Aug 2025 08:14:37 +0200 Subject: [PATCH 53/61] Add FixedExpenses components, new category to spendingsSchema and react icons --- backend/models/Spendings.js | 2 +- frontend/src/components/pages/Dashboard.jsx | 67 +++++++---- .../src/components/pages/FixedExpenses.jsx | 111 ++++++++++++++++++ .../components/pages/SpendingsManagement.jsx | 1 + .../src/components/pages/VariableExpenses.jsx | 43 ++++--- 5 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/pages/FixedExpenses.jsx diff --git a/backend/models/Spendings.js b/backend/models/Spendings.js index a2f1528aa8..71cb6658dd 100644 --- a/backend/models/Spendings.js +++ b/backend/models/Spendings.js @@ -4,7 +4,7 @@ const spendingsSchema = new mongoose.Schema({ category: { type: String, required: true, - enum: ["Rent", "Insurance", "Broadband", "Phone", "Grocery", "Shopping", "Entertainment", "Restaurants", "Cafe", "Travel", "Others"] + enum: ["Rent", "Parking", "Insurance", "Broadband", "Phone", "Grocery", "Shopping", "Entertainment", "Restaurants", "Cafe", "Travel", "Others"] }, amount: { type: Number, diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index b93c3eba5f..962c812a96 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -6,6 +6,7 @@ import { FaCoins } from 'react-icons/fa'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import { theme } from '../../styles/theme'; import VariableExpenses from './VariableExpenses'; +import FixedExpenses from './FixedExpenses'; const Navbar = styled.nav` display: flex; @@ -65,6 +66,9 @@ const LeftPanel = styled.div` margin-top: ${({ theme }) => theme.spacing(4)}; margin-bottom: ${({ theme }) => theme.spacing(2)}; } + + @media (max-width: 767px) { + display: none; // Hide left panel on mobile `; const TabButton = styled.button` @@ -91,15 +95,11 @@ const RightContent = styled.div` flex: 1; padding: ${({ theme }) => theme.spacing(4)}; display: grid; - gap: ${({ theme }) => theme.spacing(4)}; - grid-template-columns: repeat(1, 1fr); // Default to one column + gap: ${({ theme }) => theme.spacing(2)}; + grid-template-columns: 40% 60%; // Default to one column - @media (min-width: 768px) and (max-width: 1023px) { - grid-template-columns: repeat(1, 1fr); // Two columns on tablet - } - - @media (min-width: 1024px) { - grid-template-columns: repeat(2, 1fr); // Two columns on desktop + @media (max-width: 1023px) { + grid-template-columns: 1fr; // Two columns on tablet } `; @@ -110,8 +110,9 @@ const Section = styled.div` padding: ${({ theme }) => theme.spacing(4)}; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - h2 { + h3 { margin-bottom: ${({ theme }) => theme.spacing(2)}; + font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; } &.chart-section { @@ -147,13 +148,15 @@ const Card = styled.div` box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); h4 { - font-size: 1.25rem; - margin-bottom: ${({ theme }) => theme.spacing(2)}; + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; } p { - font-size: 1rem; - margin-bottom: ${({ theme }) => theme.spacing(2)}; + font-size: 1.25rem; + font-weight: ${({ theme }) => theme.typography.fontWeightSemiBold}; + margin: 0; } span { @@ -295,22 +298,46 @@ const Dashboard = () => { - - - - + + { + if (value >= 1000) { + return `${(value / 1000).toFixed(1)}K`; // Format numbers as "K" + } + return value; + }} + tick={{ + fill: theme.colors.greyLight, // Lighter gray color for the text + }} + /> + { + if (value >= 1000) { + return `${(value / 1000).toFixed(1)}K`; // Format tooltip numbers as "K" + } + return value; + }} + /> +
-

Varable Expenses

+

Variable Expenses

-

Section 4

-

Content for section 4.

+

Fixed Expenses

+
diff --git a/frontend/src/components/pages/FixedExpenses.jsx b/frontend/src/components/pages/FixedExpenses.jsx new file mode 100644 index 0000000000..c1d1b40cb7 --- /dev/null +++ b/frontend/src/components/pages/FixedExpenses.jsx @@ -0,0 +1,111 @@ +import React from 'react'; +import styled from 'styled-components'; +import { theme } from '../../styles/theme'; +import { FiHome, FiWifi } from 'react-icons/fi'; +import { AiOutlineSafetyCertificate } from 'react-icons/ai'; +import { BiMobileAlt } from 'react-icons/bi'; +import MascotImageFile from '../../assets/images/Mascot.svg'; + +const CardContainer = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: ${({ theme }) => theme.spacing(1)}; + `; + +const Card = styled.div` + background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; + text-align: left; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + flex: 1 1 calc(25% - ${({ theme }) => theme.spacing(2)}); + min-width: 100px; + + height: auto; + + h4 { + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; + margin: ${({ theme }) => theme.spacing(1)} 0; + } + + p { + font-size: 1rem; + margin: 0; + } +`; + +const SummaryContainer = styled.div` + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.greyLight}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-top: ${({ theme }) => theme.spacing(4)}; + color: ${({ theme }) => theme.colors.black}; + display: flex; + align-items: center; + justify-content: space-between; + + h3 { + margin-bottom: ${({ theme }) => theme.spacing(2)}; + font-size: 1.25rem; + font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; + } + + p { + font-size: 1rem; + line-height: 1.5; + margin: 0; +`; + +const MascotImage = styled.img` + width: 100px; + height: auto; + margin-top: ${({ theme }) => theme.spacing(4)}; + transform: scaleX(-1); +`; + +const FixedExpenses = () => { + return ( +
+ + + +

Rent

+

SEK 9600

+
+ + +

Parking

+

SEK 1000

+
+ + +

Insurance

+

SEK 400

+
+ + +

Broadband

+

SEK 500

+
+
+ +
+

Monthly Summary

+

+ You have saved 7000 SEK towards your “Trip to Quebec”, that is already 20% of your goal! This month, you spent a + bit more on food than last, but your variable expenses were still lower than average. Great job overall, you + ended up with a positive balance of 7 000 SEK this month! +

+
+ +
+
+ ); +} + +export default FixedExpenses; \ No newline at end of file diff --git a/frontend/src/components/pages/SpendingsManagement.jsx b/frontend/src/components/pages/SpendingsManagement.jsx index 17f79fe782..977279e762 100644 --- a/frontend/src/components/pages/SpendingsManagement.jsx +++ b/frontend/src/components/pages/SpendingsManagement.jsx @@ -203,6 +203,7 @@ const SpendingsManagement = () => { > + diff --git a/frontend/src/components/pages/VariableExpenses.jsx b/frontend/src/components/pages/VariableExpenses.jsx index d10c75f9ff..5ef07505e1 100644 --- a/frontend/src/components/pages/VariableExpenses.jsx +++ b/frontend/src/components/pages/VariableExpenses.jsx @@ -2,8 +2,8 @@ import React from 'react'; import styled from 'styled-components'; import { theme } from '../../styles/theme'; import { FiHome } from 'react-icons/fi'; -import { MdOutlineMovie } from 'react-icons/md'; -import { AiOutlineShoppingCart, AiOutlineSafetyCertificate } from 'react-icons/ai'; +import { MdOutlineMovie, MdOutlineRestaurant, MdOutlineLocalCafe } from 'react-icons/md'; +import { AiOutlineShoppingCart, AiOutlineCar, AiOutlineSafetyCertificate } from 'react-icons/ai'; import { BiMobileAlt } from 'react-icons/bi'; @@ -53,7 +53,7 @@ const Card = styled.div` h4 { font-size: 1rem; - font-weight: ${({ theme }) => theme.typography.fontWeightSemiBold}; + font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; } p { @@ -70,26 +70,35 @@ const VariableExpenses = () => { -

Rent

-

SEK 0

+

Grocery

+

SEK 2500

- + -

Insurance

-

SEK 0

+

Shopping

+

SEK 500

- + -

Grocery

-

SEK 0

+

Restaurants

+

SEK 800

+
+
+ + + + + +

Travel

+

SEK 500

@@ -98,16 +107,16 @@ const VariableExpenses = () => {

Entertainment

-

SEK 0

+

SEK 400

- - + + - -

Phone

-

SEK 0

+ +

Cafe

+

SEK 400

From acd68f04e36873df6ba44fbdd20e7f112efc47da Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 31 Aug 2025 13:57:32 +0200 Subject: [PATCH 54/61] Add month and createdAt to Income schema, implement GET /income/months and DELETE /income, and fix bgColor warning --- backend/models/Income.js | 9 +- backend/routes/incomeRoutes.js | 49 ++++--- frontend/src/components/pages/Dashboard.jsx | 10 +- .../src/components/pages/FixedExpenses.jsx | 10 +- .../src/components/pages/IncomeManagement.jsx | 120 ++++++++++++++++-- .../src/components/pages/VariableExpenses.jsx | 30 ++--- 6 files changed, 178 insertions(+), 50 deletions(-) diff --git a/backend/models/Income.js b/backend/models/Income.js index 1a1f448236..bb7a19def2 100644 --- a/backend/models/Income.js +++ b/backend/models/Income.js @@ -6,7 +6,14 @@ const incomeSchema = new mongoose.Schema({ required: true, default: 0, }, + month: { + type: String, + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + }, }); - export const Income = mongoose.model("Income", incomeSchema); \ No newline at end of file diff --git a/backend/routes/incomeRoutes.js b/backend/routes/incomeRoutes.js index 993e0fcad3..d4c6d8359b 100644 --- a/backend/routes/incomeRoutes.js +++ b/backend/routes/incomeRoutes.js @@ -1,5 +1,6 @@ import express from "express"; import { Income } from "../models/Income.js"; +import mongoose from "mongoose"; const router = express.Router(); @@ -17,36 +18,52 @@ router.get("/", async (req, res) => { } }); -// POST /income - Add or update the income +// GET /income/months +router.get("/months", async (req, res) => { + try { + const months = await Income.find({}, '_id month amount createdAt').sort({ createdAt: -1 }); + res.json(months); + } catch (error) { + console.error("Error fetching months:", error); + res.status(500).json({ message: "Failed to fetch months"}); + } +}); + +// POST /income - Add a new income router.post("/", async (req, res) => { - const { income } = req.body; + const { amount, month } = req.body; - if (typeof income !== "number" || income < 0) { + if (typeof amount !== "number" || amount < 0) { return res.status(400).json({ error: "Invalid income value" }); } + if (!month || typeof month !== "string") { + return res.status(400).json({ error: "Invalid month value" }); + } + try { - let incomeData = await Income.findOne(); - if (!incomeData) { - incomeData = await Income.create({ amount: income }); - } else { - incomeData.amount = income; - await incomeData.save(); - } - res.status(200).json({ message: "Income updated successfully", income: incomeData.amount }); + const newIncome = await Income.create({month, amount}); + res.status(201).json(newIncome); } catch (error) { - console.error("Error updating income:", error); - res.status(500).json({ error: "Server error" }); + res.status(400).json({ message: "Failed to add income", error: error.message }); } }); // DELETE /income - Delete the income -router.delete("/", async (req, res) => { +router.delete("/:id", async (req, res) => { + const { id } = req.params; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid income ID" }); + } try { - await Income.deleteMany(); // Remove all income records + const deletedIncome = await Income.findByIdAndDelete(id); + if (!deletedIncome) { + return res.status(404).json({ message: "Income not found" }); + } res.status(200).json({ message: "Income deleted successfully" }); } catch (error) { - console.error("Error deleting income:", error); + console.error("Error deleting income:", error.message, error.stack); return res.status(500).json({ error: "Server error" }); } }); diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index 962c812a96..066158b52b 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -140,7 +140,7 @@ const CardContainer = styled.div` `; const Card = styled.div` - background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; + background-color: ${({ $bgColor, theme }) => $bgColor || theme.colors.greyLight}; border: 1px solid ${({ theme }) => theme.colors.grey}; border-radius: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(4)}; @@ -259,7 +259,7 @@ const Dashboard = () => {
- + @@ -267,7 +267,7 @@ const Dashboard = () => {

SEK 0

Additional Info
- + navigate('/spendings-management')} > @@ -275,7 +275,7 @@ const Dashboard = () => {

SEK {loading ? 'Loading...' : totalSpendings}

Additional Info
- + @@ -283,7 +283,7 @@ const Dashboard = () => {

SEK 0

Additional Info
- + navigate('/income-management')} > diff --git a/frontend/src/components/pages/FixedExpenses.jsx b/frontend/src/components/pages/FixedExpenses.jsx index c1d1b40cb7..7c3258b402 100644 --- a/frontend/src/components/pages/FixedExpenses.jsx +++ b/frontend/src/components/pages/FixedExpenses.jsx @@ -14,7 +14,7 @@ const CardContainer = styled.div` `; const Card = styled.div` - background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; + background-color: ${({ $bgColor, theme }) => $bgColor || theme.colors.greyLight}; border: 1px solid ${({ theme }) => theme.colors.grey}; border-radius: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)}; @@ -72,22 +72,22 @@ const FixedExpenses = () => { return (
- +

Rent

SEK 9600

- +

Parking

SEK 1000

- +

Insurance

SEK 400

- +

Broadband

SEK 500

diff --git a/frontend/src/components/pages/IncomeManagement.jsx b/frontend/src/components/pages/IncomeManagement.jsx index e319ffb694..5ed94681e0 100644 --- a/frontend/src/components/pages/IncomeManagement.jsx +++ b/frontend/src/components/pages/IncomeManagement.jsx @@ -1,7 +1,9 @@ import React, { useState } from 'react'; +import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import { FiX } from 'react-icons/fi'; const Navbar = styled.nav` display: flex; @@ -76,41 +78,122 @@ const SaveButton = styled(Button)` } `; +const CardContainer = styled.div` + display: grid; + gap: ${({ theme }) => theme.spacing(4)}; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + margin-top: ${({ theme }) => theme.spacing(4)}; +`; + +const Card = styled.div` + background-color: ${({ theme }) => theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: relative; + + h4 { + margin-bottom: ${({ theme }) => theme.spacing(2)}; + } + + p { + margin: 0; + + .actions { + margin-top: ${({ theme }) => theme.spacing(2)}; + display: flex; + justify-content: space-between; +} +`; + +const DeleteIcon = styled.button` + position: absolute; + top: ${({ theme }) => theme.spacing(2)}; + right: ${({ theme }) => theme.spacing(2)}; + background: none; + border: none; + color: ${({ theme }) => theme.colors.black}; + font-size: 1rem; + cursor: pointer; +`; + const IncomeManagement = () => { const navigate = useNavigate(); const [income, setIncome] = useState(''); + const [months, setMonths] = useState([]); + const [month, setMonth] = useState(''); + + useEffect(() => { + const fetchMonths = async () => { + try { + const response = await fetch('http://localhost:8080/income/months'); + if (!response.ok) { + throw new Error('Failed to fetch months'); + } + const data = await response.json(); + setMonths(data); + } catch (error) { + console.error('Error fetching months:', error); + } + }; + + fetchMonths(); + }, []); const handleSubmit = async (e) => { e.preventDefault(); if (income <= 0) { - alert('Please enter a valid income value greater than 0.'); + alert('Please enter a valid income greater than 0.'); + return; + } + + if (!month) { + alert('Please enter a valid month.'); return; } try { - await saveIncomeToAPI(Number(income)); - console.log('Income submitted:', income); - - navigate('/dashboard'); // Navigate back to the dashboard after submission + await saveIncomeToAPI(Number(income), month); + console.log('Income submitted:', income, month); + navigate('/dashboard'); } catch (error) { console.error('Error submitting income:', error); alert('Failed to save income. Please try again.'); } }; - const saveIncomeToAPI = async (income) => { + const saveIncomeToAPI = async (income, month) => { const response = await fetch('http://localhost:8080/income', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ income }), + body: JSON.stringify({ amount: income, month }), }); if (!response.ok) { throw new Error('Failed to save income'); } + + const newIncome = await response.json(); // Parse the response to get the new spending + return newIncome; + }; + + const handleDelete = async (id) => { + const confirmDelete = window.confirm('Are you sure you want to delete this month?'); + if (!confirmDelete) return; + + try { + await fetch(`http://localhost:8080/income/${id}`, { + method: 'DELETE', + }); + setMonths((prevMonths) => prevMonths.filter((month) => month._id !== id)); + } catch (error) { + console.error('Error deleting month:', error); + alert('Failed to delete month. Please try again.'); + } }; return ( @@ -124,17 +207,38 @@ const IncomeManagement = () => {

Manage Income

+ + setMonth(e.target.value)} + placeholder="e.g. January" + required + /> setIncome(e.target.value)} - placeholder="Enter your income after tax" + placeholder="e.g. 26000" required /> Save Income
+ + {months.map((month) => ( + + handleDelete(month._id)}> + + +

Month: {month.month}

+

Amount: SEK {month.amount}

+

Date: {month.createdAt ? new Date(month.createdAt).toLocaleDateString() : "N/A"}

+
+ ))} +
); diff --git a/frontend/src/components/pages/VariableExpenses.jsx b/frontend/src/components/pages/VariableExpenses.jsx index 5ef07505e1..c74997a460 100644 --- a/frontend/src/components/pages/VariableExpenses.jsx +++ b/frontend/src/components/pages/VariableExpenses.jsx @@ -26,8 +26,8 @@ const Quadrant = styled.div` justify-content: center; width: 48px; height: 48px; - background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; - border: 1px solid ${({ theme }) => theme.colors.grey}; + background-color: ${({ $bgColor, theme }) => $bgColor || theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; border-radius: ${({ theme }) => theme.spacing(2)}; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); @@ -44,7 +44,7 @@ const Card = styled.div` align-items: center; justify-content: space-between; gap: ${({ theme }) => theme.spacing(2)}; - background-color: ${({ bgColor, theme }) => bgColor || theme.colors.greyLight}; + background-color: ${({ $bgColor, theme }) => $bgColor || theme.colors.greyLight}; border: 1px solid ${({ theme }) => theme.colors.grey}; border-radius: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(4)}; @@ -66,55 +66,55 @@ const VariableExpenses = () => { <> - + - +

Grocery

SEK 2500

- + - +

Shopping

SEK 500

- + - +

Restaurants

SEK 800

- + - +

Travel

SEK 500

- + - +

Entertainment

SEK 400

- + - +

Cafe

SEK 400

From 4954d965b0fdbf7d19e224c006d49f2525b2407b Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 31 Aug 2025 16:54:09 +0200 Subject: [PATCH 55/61] Add delete functionality for spendings --- backend/routes/spendingsRoutes.js | 15 ++++++++ frontend/src/components/pages/Dashboard.jsx | 36 +++++++++++++------ .../components/pages/SpendingsManagement.jsx | 17 ++++++++- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/backend/routes/spendingsRoutes.js b/backend/routes/spendingsRoutes.js index 1ddedc7fbc..13311e39ac 100644 --- a/backend/routes/spendingsRoutes.js +++ b/backend/routes/spendingsRoutes.js @@ -39,4 +39,19 @@ router.get("/total", async (req, res) => { } }); +router.delete("/:id", async (req, res) => { + const { id } = req.params; + + try { + const deletedSpending = await Spendings.findByIdAndDelete(id); + if (!deletedSpending) { + return res.status(404).json({ message: "Spending not found" }); + } + res.status(200).json({ message: "Spending deleted successfully" }); + } catch (error) { + console.error("Error deleting spending:", error.message, error.stack); + return res.status(500).json({ error: "Server error" }); + } +}); + export default router; \ No newline at end of file diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index 066158b52b..358bfefc98 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -173,20 +173,11 @@ const Card = styled.div` } `; -// Sample data for the bar chart -const data = [ - { name: 'Jan', Income: 25000, Spendings: 18500 }, - { name: 'Feb', Income: 25000, Spendings: 19300 }, - { name: 'Mar', Income: 25000, Spendings: 19800 }, - { name: 'Apr', Income: 25000, Spendings: 19000 }, - { name: 'May', Income: 25000, Spendings: 19800 }, - { name: 'Jun', Income: 25000, Spendings: 20800 }, -]; - const Dashboard = () => { const [income, setIncome] = useState(0); const [totalSpendings, setTotalSpendings] = useState(0); const [loading, setLoading] = useState(true); + const [chartData, setChartData] = useState([]); const navigate = useNavigate(); const handleLogout = () => { @@ -232,6 +223,29 @@ const Dashboard = () => { } }; + useEffect(() => { + const fetchChartData = async () => { + try { + const response = await fetch('http://localhost:8080/income/months'); + if (!response.ok) { + throw new Error('Failed to fetch months data'); + } + const monthsData = await response.json(); + const data = monthsData.map((monthData) => ({ + name: monthData.month, + Income: monthData.amount || 0, + Spendings: totalSpendings, // Use totalSpendings state + })); + setChartData(data); + } catch (error) { + console.error('Error fetching months data:', error); + } + }; + fetchChartData(); + }, [totalSpendings]); // Re-fetch chart data when totalSpendings changes + + + return ( <> @@ -296,7 +310,7 @@ const Dashboard = () => {

Balance

- + { return newSpending; }; + const handleDelete = async (id) => { + const confirmDelete = window.confirm('Are you sure you want to delete this spending?'); + if (!confirmDelete) return; + + try { + await fetch(`http://localhost:8080/spendings/${id}`, { + method: 'DELETE', + }); + setSpendings((prevSpendings) => prevSpendings.filter((spending) => spending._id !== id)); + } catch (error) { + console.error('Error deleting spending:', error); + alert('Failed to delete spending. Please try again.'); + } + }; + return ( <> @@ -229,7 +244,7 @@ const SpendingsManagement = () => { {spendings.map((spending) => ( - + handleDelete(spending._id)}>

{capitalizeFirstLetter(spending.category)}

From 16dba090092b3eb2487098ed7bc800e866bd9e40 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 31 Aug 2025 17:11:41 +0200 Subject: [PATCH 56/61] Display dynamic balance between income - spendings, and add dynamic loading states for balance and spendings cards --- frontend/src/components/pages/Dashboard.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index 358bfefc98..ab2cbc454e 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -180,6 +180,8 @@ const Dashboard = () => { const [chartData, setChartData] = useState([]); const navigate = useNavigate(); + const balance = income - totalSpendings; + const handleLogout = () => { localStorage.removeItem('accessToken'); navigate('/'); @@ -244,8 +246,6 @@ const Dashboard = () => { fetchChartData(); }, [totalSpendings]); // Re-fetch chart data when totalSpendings changes - - return ( <> @@ -278,7 +278,7 @@ const Dashboard = () => {

Balance

-

SEK 0

+ {loading ? null :

SEK {balance}

} Additional Info
@@ -286,7 +286,7 @@ const Dashboard = () => {

Spendings

-

SEK {loading ? 'Loading...' : totalSpendings}

+ {loading ? null :

SEK {totalSpendings}

} Additional Info
From 07cbf7a1af5b10a75e24a24a47cc1021845ecff5 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 31 Aug 2025 18:05:06 +0200 Subject: [PATCH 57/61] Modularize dashboard by creating separate files for Navbar, LeftPanel, DashboardCard and DashboardChart --- frontend/src/components/pages/Dashboard.jsx | 233 +----------------- .../src/components/pages/DashboardCards.jsx | 96 ++++++++ .../src/components/pages/DashboardChart.jsx | 46 ++++ frontend/src/components/pages/LeftPanel.jsx | 63 +++++ frontend/src/components/pages/Navbar.jsx | 56 +++++ 5 files changed, 274 insertions(+), 220 deletions(-) create mode 100644 frontend/src/components/pages/DashboardCards.jsx create mode 100644 frontend/src/components/pages/DashboardChart.jsx create mode 100644 frontend/src/components/pages/LeftPanel.jsx create mode 100644 frontend/src/components/pages/Navbar.jsx diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index ab2cbc454e..f79376eea8 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -1,96 +1,18 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import { FiBarChart2, FiCreditCard, FiTrendingUp, FiEdit } from 'react-icons/fi'; -import { FaCoins } from 'react-icons/fa'; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; -import { theme } from '../../styles/theme'; import VariableExpenses from './VariableExpenses'; import FixedExpenses from './FixedExpenses'; - -const Navbar = styled.nav` - display: flex; - justify-content: space-between; - align-items: center; - padding: ${({ theme }) => theme.spacing(4)}; - border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; - background-color: ${({ theme }) => theme.colors.primary}; - width: 100%; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -`; - -const Logo = styled.h1` - font-size: 1rem; - font-weight: ${({ theme }) => theme.typography.fontWeightBold}; - color: ${({ theme }) => theme.colors.white}; - margin: 0; -`; - -const NavButtons = styled.div` - display: flex; - gap: ${({ theme }) => theme.spacing(4)}; - - @media (max-width: 767px) { - display: none; - } -`; - -const Button = styled.button` - background-color: ${({ theme }) => theme.colors.pink}; - color: ${({ theme }) => theme.colors.black}; - padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; - font-size: ${({ theme }) => theme.typography.fontSize}; - border: none; - border-radius: ${({ theme }) => theme.spacing(2)}; - cursor: pointer; - - &:hover { - background-color: ${({ theme }) => theme.colors.limeGreenLight}; - } -`; +import DashboardCards from './DashboardCards'; +import DashboardChart from './DashboardChart'; +import LeftPanel from './LeftPanel'; +import Navbar from './Navbar'; const DashboardContainer = styled.div` display: flex; height: calc(100vh - ${({ theme }) => theme.spacing(8)}); // Full height minus navbar height `; -const LeftPanel = styled.div` - width: 190px; - background-color: ${({ theme }) => theme.colors.white}; - padding: ${({ theme }) => theme.spacing(4)}; - display: flex; - flex-direction: column; - gap: ${({ theme }) => theme.spacing(1)}; - - h2 { - margin-top: ${({ theme }) => theme.spacing(4)}; - margin-bottom: ${({ theme }) => theme.spacing(2)}; - } - - @media (max-width: 767px) { - display: none; // Hide left panel on mobile -`; - -const TabButton = styled.button` - background-color: ${({ theme }) => theme.colors.greyLight}; - color: ${({ theme }) => theme.colors.black}; - padding: ${({ theme }) => theme.spacing(3)}; - font-size: 1rem; - border: none; - border-radius: ${({ theme }) => theme.spacing(2)}; - cursor: pointer; - text-align: left; - width: 100%; - display: flex; - align-items: center; - gap: ${({ theme }) => theme.spacing(1)}; - - &:hover { - background-color: ${({ theme }) => theme.colors.primaryLight}; - color: ${({ theme }) => theme.colors.white}; - } -`; - const RightContent = styled.div` flex: 1; padding: ${({ theme }) => theme.spacing(4)}; @@ -125,54 +47,6 @@ const Section = styled.div` } `; -const CardContainer = styled.div` - display: grid; - gap: ${({ theme }) => theme.spacing(4)}; - grid-template-columns: repeat(1, 1fr); // Default to one column - - @media (min-width: 768px) { - grid-template-columns: repeat(2, 1fr); // Two columns on tablet - } - - @media (min-width: 1024px) { - grid-template-columns: repeat(2, 1fr); // Two columns on desktop - } -`; - -const Card = styled.div` - background-color: ${({ $bgColor, theme }) => $bgColor || theme.colors.greyLight}; - border: 1px solid ${({ theme }) => theme.colors.grey}; - border-radius: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(4)}; - position: relative; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - - h4 { - font-size: 1rem; - font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; - margin-bottom: ${({ theme }) => theme.spacing(1)}; - } - - p { - font-size: 1.25rem; - font-weight: ${({ theme }) => theme.typography.fontWeightSemiBold}; - margin: 0; - } - - span { - font-size: 0.9rem; - color: ${({ theme }) => theme.colors.primaryDark}; - } - - .edit-icon { - position: absolute; - top: ${({ theme }) => theme.spacing(2)}; - right: ${({ theme }) => theme.spacing(2)}; - cursor: pointer; - font-size: 1rem; // Adjust the size of the emoji - } -`; - const Dashboard = () => { const [income, setIncome] = useState(0); const [totalSpendings, setTotalSpendings] = useState(0); @@ -248,102 +122,21 @@ const Dashboard = () => { return ( <> - - StoryBudget - - - - + - -

Dashboard

- - Balance - - - Spendings - - - Savings - - - Income - -
+
- - - - - -

Balance

- {loading ? null :

SEK {balance}

} - Additional Info -
- - navigate('/spendings-management')} > - - -

Spendings

- {loading ? null :

SEK {totalSpendings}

} - Additional Info -
- - - - -

Savings

-

SEK 0

- Additional Info -
- - navigate('/income-management')} > - - -

Income

- {loading ? null :

SEK {income}

} - Additional Info -
-
+

Balance

- - - - - { - if (value >= 1000) { - return `${(value / 1000).toFixed(1)}K`; // Format numbers as "K" - } - return value; - }} - tick={{ - fill: theme.colors.greyLight, // Lighter gray color for the text - }} - /> - { - if (value >= 1000) { - return `${(value / 1000).toFixed(1)}K`; // Format tooltip numbers as "K" - } - return value; - }} - /> - - - - - +

Variable Expenses

diff --git a/frontend/src/components/pages/DashboardCards.jsx b/frontend/src/components/pages/DashboardCards.jsx new file mode 100644 index 0000000000..d7e516d9ad --- /dev/null +++ b/frontend/src/components/pages/DashboardCards.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { FiEdit } from 'react-icons/fi'; +import styled from 'styled-components'; +import { theme } from '../../styles/theme'; + +const CardContainer = styled.div` + display: grid; + gap: ${({ theme }) => theme.spacing(4)}; + grid-template-columns: repeat(1, 1fr); // Default to one column + + @media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); // Two columns on tablet + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(2, 1fr); // Two columns on desktop + } +`; + +const Card = styled.div` + background-color: ${({ $bgColor, theme }) => $bgColor || theme.colors.greyLight}; + border: 1px solid ${({ theme }) => theme.colors.grey}; + border-radius: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(4)}; + position: relative; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + h4 { + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightMedium}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; + } + + p { + font-size: 1.25rem; + font-weight: ${({ theme }) => theme.typography.fontWeightSemiBold}; + margin: 0; + } + + span { + font-size: 0.9rem; + color: ${({ theme }) => theme.colors.primaryDark}; + } + + .edit-icon { + position: absolute; + top: ${({ theme }) => theme.spacing(2)}; + right: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + font-size: 1rem; // Adjust the size of the emoji + } +`; + +const DashboardCards = ({ balance, totalSpendings, income, loading }) => { + const navigate = useNavigate(); + + return ( + + + + + +

Balance

+ {loading ? null :

SEK {balance}

} + Additional Info +
+ + navigate('/spendings-management')} > + + +

Spendings

+ {loading ? null :

SEK {totalSpendings}

} + Additional Info +
+ + + + +

Savings

+

SEK 0

+ Additional Info +
+ + navigate('/income-management')} > + + +

Income

+ {loading ? null :

SEK {income}

} + Additional Info +
+
+ ); +} + +export default DashboardCards; diff --git a/frontend/src/components/pages/DashboardChart.jsx b/frontend/src/components/pages/DashboardChart.jsx new file mode 100644 index 0000000000..edd474a9e8 --- /dev/null +++ b/frontend/src/components/pages/DashboardChart.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { theme } from '../../styles/theme'; + +const DashboardChart = ({ chartData }) => { + return ( + + + + + { + if (value >= 1000) { + return `${(value / 1000).toFixed(1)}K`; // Format numbers as "K" + } + return value; + }} + tick={{ + fill: theme.colors.greyLight, // Lighter gray color for the text + }} + /> + { + if (value >= 1000) { + return `${(value / 1000).toFixed(1)}K`; // Format tooltip numbers as "K" + } + return value; + }} + /> + + + + + + ); +} + +export default DashboardChart; \ No newline at end of file diff --git a/frontend/src/components/pages/LeftPanel.jsx b/frontend/src/components/pages/LeftPanel.jsx new file mode 100644 index 0000000000..48a9c3eee4 --- /dev/null +++ b/frontend/src/components/pages/LeftPanel.jsx @@ -0,0 +1,63 @@ +import React from "react"; +import styled from "styled-components"; +import { FiBarChart2, FiCreditCard, FiTrendingUp } from 'react-icons/fi'; +import { FaCoins } from 'react-icons/fa'; + +const LeftPanelContainer = styled.div` + width: 190px; + background-color: ${({ theme }) => theme.colors.white}; + padding: ${({ theme }) => theme.spacing(4)}; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(1)}; + + h2 { + margin-top: ${({ theme }) => theme.spacing(4)}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + } + + @media (max-width: 767px) { + display: none; // Hide left panel on mobile +`; + +const TabButton = styled.button` + background-color: ${({ theme }) => theme.colors.greyLight}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(3)}; + font-size: 1rem; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + text-align: left; + width: 100%; + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; + + &:hover { + background-color: ${({ theme }) => theme.colors.primaryLight}; + color: ${({ theme }) => theme.colors.white}; + } +`; + +const LeftPanel = () => { + return ( + +

Dashboard

+ + Balance + + + Spendings + + + Savings + + + Income + +
+ ); +}; + +export default LeftPanel; \ No newline at end of file diff --git a/frontend/src/components/pages/Navbar.jsx b/frontend/src/components/pages/Navbar.jsx new file mode 100644 index 0000000000..6d9a5dd5f7 --- /dev/null +++ b/frontend/src/components/pages/Navbar.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import styled from 'styled-components'; + +const NavbarContainer = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + padding: ${({ theme }) => theme.spacing(4)}; + border-bottom: 1px solid ${({ theme }) => theme.colors.greyLight}; + background-color: ${({ theme }) => theme.colors.primary}; + width: 100%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +`; + +const Logo = styled.h1` + font-size: 1rem; + font-weight: ${({ theme }) => theme.typography.fontWeightBold}; + color: ${({ theme }) => theme.colors.white}; + margin: 0; +`; + +const NavButtons = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; + + @media (max-width: 767px) { + display: none; + } +`; + +const Button = styled.button` + background-color: ${({ theme }) => theme.colors.pink}; + color: ${({ theme }) => theme.colors.black}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(4)}; + font-size: ${({ theme }) => theme.typography.fontSize}; + border: none; + border-radius: ${({ theme }) => theme.spacing(2)}; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.colors.limeGreenLight}; + } +`; + +const Navbar = ({onLogout}) => { + return ( + + StoryBudget + + + + + ); +}; + +export default Navbar; \ No newline at end of file From f9fc0a6847f181175884915afb842fc9bbc11868 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 31 Aug 2025 18:09:56 +0200 Subject: [PATCH 58/61] Removed unnecessary images from assets --- frontend/public/vite.svg | 1 - frontend/src/assets/boiler-plate.svg | 18 ---------------- frontend/src/assets/react.svg | 1 - frontend/src/assets/technigo-logo.svg | 31 --------------------------- 4 files changed, 51 deletions(-) delete mode 100644 frontend/public/vite.svg delete mode 100644 frontend/src/assets/boiler-plate.svg delete mode 100644 frontend/src/assets/react.svg delete mode 100644 frontend/src/assets/technigo-logo.svg diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb1b2..0000000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/boiler-plate.svg b/frontend/src/assets/boiler-plate.svg deleted file mode 100644 index c9252833b4..0000000000 --- a/frontend/src/assets/boiler-plate.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/technigo-logo.svg b/frontend/src/assets/technigo-logo.svg deleted file mode 100644 index 3f0da3e572..0000000000 --- a/frontend/src/assets/technigo-logo.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 56507d453c5d54680a305eb86d6c9b192e605132 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 31 Aug 2025 21:39:10 +0200 Subject: [PATCH 59/61] Add and refine ProtectedRoute component to remove sensitive information from console logs --- backend/routes/userRoutes.js | 27 ++++++++++++++++--- frontend/src/App.jsx | 9 ++++--- frontend/src/components/pages/Login.jsx | 2 +- .../src/components/pages/ProtectedRoute.jsx | 11 ++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/pages/ProtectedRoute.jsx diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 167abf9d30..cad1547ad1 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -2,10 +2,7 @@ import express from 'express'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { User } from '../models/User.js'; -import dotenv from 'dotenv'; - -dotenv.config(); // Load environment variables -console.log('JWT_SECRET:', process.env.JWT_SECRET); +import { authenticateUser } from '../middleware/authMiddleware.js'; const router = express.Router() const JWT_SECRET = process.env.JWT_SECRET || 'your_secure_secret_key'; // Use environment variable for the secret key @@ -108,5 +105,27 @@ router.post("/login", async (req, res) => { } }) + // GET - Protected dashboard route + router.get("/dashboard", authenticateUser, async (req, res) => { + try { + res.status(200).json({ + success: true, + message: "User authenticated successfully.", + response: { + id: req.user._id, + name: req.user.name, + email: req.user.email, + } + }) + } catch (error) { + console.error("Dashboard error:", error) + res.status(500).json({ + success: false, + message: "Failed to access dashboard.", + response: error.message + }) + } + }) + export default router \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b98811add0..b8116809fc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,13 +1,14 @@ import { ThemeProvider } from 'styled-components'; import { theme } from './styles/theme'; import { GlobalStyles } from './styles/globalStyles'; -import { Routes, Route } from 'react-router-dom'; +import { Routes, Route, Router } from 'react-router-dom'; import Landing from './components/pages/Landing'; import Signup from './components/pages/Signup'; import Login from './components/pages/Login'; import Dashboard from './components/pages/Dashboard'; import IncomeManagement from './components/pages/IncomeManagement'; import SpendingsManagement from './components/pages/SpendingsManagement'; +import ProtectedRoute from './components/pages/ProtectedRoute'; export const App = () => { @@ -18,9 +19,9 @@ export const App = () => { } /> } /> } /> - } /> - } /> - } /> + } /> + }/> + }/> ); diff --git a/frontend/src/components/pages/Login.jsx b/frontend/src/components/pages/Login.jsx index c89a90a4c2..41769ddbc1 100644 --- a/frontend/src/components/pages/Login.jsx +++ b/frontend/src/components/pages/Login.jsx @@ -246,7 +246,7 @@ const Login = () => { email, password }); - console.log(response.data); + console.log("Login successful"); // Store the accessToken in localStorage localStorage.setItem('accessToken', response.data.response.accessToken); diff --git a/frontend/src/components/pages/ProtectedRoute.jsx b/frontend/src/components/pages/ProtectedRoute.jsx new file mode 100644 index 0000000000..6b6963e3f9 --- /dev/null +++ b/frontend/src/components/pages/ProtectedRoute.jsx @@ -0,0 +1,11 @@ +import React from "react"; +import { Navigate } from "react-router-dom"; + +const ProtectedRoute = ({ children }) => { + const accessToken = localStorage.getItem("accessToken"); + if (!accessToken) { + return ; + } + return children; +} +export default ProtectedRoute; \ No newline at end of file From bc572ec2051028dc5d132c0a7bffef97b1238556 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 26 Oct 2025 12:01:32 +0100 Subject: [PATCH 60/61] change to backend url in .env file and change console.log --- frontend/src/components/pages/Signup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx index c9fa750bec..79a52aed63 100644 --- a/frontend/src/components/pages/Signup.jsx +++ b/frontend/src/components/pages/Signup.jsx @@ -249,7 +249,7 @@ const Signup = () => { email, password }); - console.log(response.data); + console.log("Signup successful"); // Store the accessToken in localStorage localStorage.setItem('accessToken', response.data.response.accessToken); From 323b8414e3059f9208bcedad0c0022c56c4c4589 Mon Sep 17 00:00:00 2001 From: Tavan Thiry Date: Sun, 26 Oct 2025 13:08:10 +0100 Subject: [PATCH 61/61] change to point API calls to deployed backend URL --- frontend/src/components/pages/Dashboard.jsx | 6 +++--- frontend/src/components/pages/IncomeManagement.jsx | 6 +++--- frontend/src/components/pages/SpendingsManagement.jsx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx index f79376eea8..5745a66cfd 100644 --- a/frontend/src/components/pages/Dashboard.jsx +++ b/frontend/src/components/pages/Dashboard.jsx @@ -75,7 +75,7 @@ const Dashboard = () => { const fetchIncome = async () => { try { - const response = await fetch('http://localhost:8080/income'); + const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/income`); if (!response.ok) { throw new Error('Failed to fetch income'); } @@ -88,7 +88,7 @@ const Dashboard = () => { const fetchTotalSpendings = async () => { try { - const response = await fetch('http://localhost:8080/spendings/total'); + const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/spendings/total`); if (!response.ok) { throw new Error('Failed to fetch total spendings'); } @@ -102,7 +102,7 @@ const Dashboard = () => { useEffect(() => { const fetchChartData = async () => { try { - const response = await fetch('http://localhost:8080/income/months'); + const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/income/months`); if (!response.ok) { throw new Error('Failed to fetch months data'); } diff --git a/frontend/src/components/pages/IncomeManagement.jsx b/frontend/src/components/pages/IncomeManagement.jsx index 5ed94681e0..944f7d04a1 100644 --- a/frontend/src/components/pages/IncomeManagement.jsx +++ b/frontend/src/components/pages/IncomeManagement.jsx @@ -127,7 +127,7 @@ const IncomeManagement = () => { useEffect(() => { const fetchMonths = async () => { try { - const response = await fetch('http://localhost:8080/income/months'); + const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/income/months`); if (!response.ok) { throw new Error('Failed to fetch months'); } @@ -165,7 +165,7 @@ const IncomeManagement = () => { }; const saveIncomeToAPI = async (income, month) => { - const response = await fetch('http://localhost:8080/income', { + const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/income`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -186,7 +186,7 @@ const IncomeManagement = () => { if (!confirmDelete) return; try { - await fetch(`http://localhost:8080/income/${id}`, { + await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/income/${id}`, { method: 'DELETE', }); setMonths((prevMonths) => prevMonths.filter((month) => month._id !== id)); diff --git a/frontend/src/components/pages/SpendingsManagement.jsx b/frontend/src/components/pages/SpendingsManagement.jsx index 07d13a8e10..69012e155d 100644 --- a/frontend/src/components/pages/SpendingsManagement.jsx +++ b/frontend/src/components/pages/SpendingsManagement.jsx @@ -130,7 +130,7 @@ const SpendingsManagement = () => { useEffect(() => { const fetchSpendings = async () => { try { - const response = await fetch('http://localhost:8080/spendings'); + const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/spendings`); if (!response.ok) { throw new Error('Failed to fetch spendings'); } @@ -166,7 +166,7 @@ const SpendingsManagement = () => { }; const saveSpendingToAPI = async (category, amount) => { - const response = await fetch('http://localhost:8080/spendings', { + const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/spendings`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -188,7 +188,7 @@ const SpendingsManagement = () => { if (!confirmDelete) return; try { - await fetch(`http://localhost:8080/spendings/${id}`, { + await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/spendings/${id}`, { method: 'DELETE', }); setSpendings((prevSpendings) => prevSpendings.filter((spending) => spending._id !== id));