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/models/Income.js b/backend/models/Income.js
new file mode 100644
index 0000000000..bb7a19def2
--- /dev/null
+++ b/backend/models/Income.js
@@ -0,0 +1,19 @@
+import mongoose from 'mongoose'
+
+const incomeSchema = new mongoose.Schema({
+ amount: {
+ type: Number,
+ 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/models/Spendings.js b/backend/models/Spendings.js
new file mode 100644
index 0000000000..71cb6658dd
--- /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: ["Rent", "Parking", "Insurance", "Broadband", "Phone", "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/models/User.js b/backend/models/User.js
new file mode 100644
index 0000000000..92c7c31db9
--- /dev/null
+++ 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
diff --git a/backend/package.json b/backend/package.json
index 08f29f2448..585ccb1439 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"
@@ -12,9 +13,13 @@
"@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",
+ "express-list-endpoints": "^7.1.1",
+ "jsonwebtoken": "^9.0.2",
"mongoose": "^8.4.0",
"nodemon": "^3.0.1"
}
-}
\ No newline at end of file
+}
diff --git a/backend/routes/incomeRoutes.js b/backend/routes/incomeRoutes.js
new file mode 100644
index 0000000000..d4c6d8359b
--- /dev/null
+++ b/backend/routes/incomeRoutes.js
@@ -0,0 +1,71 @@
+import express from "express";
+import { Income } from "../models/Income.js";
+import mongoose from "mongoose";
+
+const router = express.Router();
+
+// GET /income - Fetch the current income
+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" });
+ }
+});
+
+// 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 { amount, month } = req.body;
+
+ 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 {
+ const newIncome = await Income.create({month, amount});
+ res.status(201).json(newIncome);
+ } catch (error) {
+ res.status(400).json({ message: "Failed to add income", error: error.message });
+ }
+});
+
+// DELETE /income - Delete the income
+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 {
+ 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.message, error.stack);
+ return res.status(500).json({ error: "Server error" });
+ }
+});
+
+export default router;
\ No newline at end of file
diff --git a/backend/routes/spendingsRoutes.js b/backend/routes/spendingsRoutes.js
new file mode 100644
index 0000000000..13311e39ac
--- /dev/null
+++ b/backend/routes/spendingsRoutes.js
@@ -0,0 +1,57 @@
+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 });
+ }
+});
+
+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/backend/routes/userRoutes.js b/backend/routes/userRoutes.js
new file mode 100644
index 0000000000..cad1547ad1
--- /dev/null
+++ b/backend/routes/userRoutes.js
@@ -0,0 +1,131 @@
+import express from 'express';
+import bcrypt from 'bcryptjs';
+import jwt from 'jsonwebtoken';
+import { User } from '../models/User.js';
+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
+
+// POST - Register a new user
+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
+ })
+
+ //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: token,
+ }
+ })
+
+ } catch (error) {
+ console.error("Signup error:", error)
+ res.status(400).json({
+ success: false,
+ message: "Failed to create user.",
+ response: error.message
+ })
+ }
+ })
+
+
+ // 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
+ })
+ }
+ })
+
+ // 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/backend/server.js b/backend/server.js
index 070c875189..6fe0cc021c 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -1,6 +1,14 @@
import express from "express";
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";
+import spendingsRoutes from "./routes/spendingsRoutes.js";
+
+dotenv.config();
+console.log('JWT_SECRET:', process.env.JWT_SECRET);
const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project";
mongoose.connect(mongoUrl);
@@ -12,9 +20,19 @@ const app = express();
app.use(cors());
app.use(express.json());
+// endpoint for documentation of the API
app.get("/", (req, res) => {
- res.send("Hello Technigo!");
-});
+ const endpoints = listEndpoints(app)
+ res.json({
+ message: "Welcome to StoryBudget API",
+ endpoints: endpoints
+ })
+})
+
+//endpoint routes
+app.use('/users', userRoutes);
+app.use('/income', incomeRoutes);
+app.use('/spendings', spendingsRoutes);
// Start the server
app.listen(port, () => {
diff --git a/frontend/index.html b/frontend/index.html
index 664410b5b9..52c5c7a1aa 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,8 @@
-
Technigo React Vite Boiler Plate
+
+ StoryBudget
diff --git a/frontend/package.json b/frontend/package.json
index 7b2747e949..ae37bdda4f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,8 +10,14 @@
"preview": "vite preview"
},
"dependencies": {
+ "axios": "^1.11.0",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "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"
},
"devDependencies": {
"@types/react": "^18.2.15",
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/App.jsx b/frontend/src/App.jsx
index 0a24275e6e..b8116809fc 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,8 +1,30 @@
+import { ThemeProvider } from 'styled-components';
+import { theme } from './styles/theme';
+import { GlobalStyles } from './styles/globalStyles';
+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 = () => {
return (
- <>
- Welcome to Final Project!
- >
+
+
+
+ } />
+ } />
+ } />
+ } />
+ }/>
+ }/>
+
+
);
};
+
+export default App;
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/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/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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/src/components/pages/Dashboard.jsx b/frontend/src/components/pages/Dashboard.jsx
new file mode 100644
index 0000000000..5745a66cfd
--- /dev/null
+++ b/frontend/src/components/pages/Dashboard.jsx
@@ -0,0 +1,155 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import styled from 'styled-components';
+import VariableExpenses from './VariableExpenses';
+import FixedExpenses from './FixedExpenses';
+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 RightContent = styled.div`
+ flex: 1;
+ padding: ${({ theme }) => theme.spacing(4)};
+ display: grid;
+ gap: ${({ theme }) => theme.spacing(2)};
+ grid-template-columns: 40% 60%; // Default to one column
+
+ @media (max-width: 1023px) {
+ grid-template-columns: 1fr; // Two columns on tablet
+ }
+`;
+
+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);
+
+ h3 {
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
+ font-weight: ${({ theme }) => theme.typography.fontWeightMedium};
+ }
+
+ &.chart-section {
+ background-color: ${({ theme }) => theme.colors.primary};
+ color: ${({ theme }) => theme.colors.white};
+ }
+
+ &.chart-section h3 {
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
+ }
+`;
+
+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 balance = income - totalSpendings;
+
+ const handleLogout = () => {
+ localStorage.removeItem('accessToken');
+ navigate('/');
+ };
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true);
+ await fetchIncome();
+ await fetchTotalSpendings();
+ setLoading(false);
+ };
+
+ fetchData();
+ }, []);
+
+
+ const fetchIncome = async () => {
+ try {
+ const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/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(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/spendings/total`);
+ if (!response.ok) {
+ throw new Error('Failed to fetch total spendings');
+ }
+ const data = await response.json();
+ setTotalSpendings(data.total);
+ } catch (error) {
+ console.error('Error fetching total spendings:', error);
+ }
+ };
+
+ useEffect(() => {
+ const fetchChartData = async () => {
+ try {
+ 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');
+ }
+ 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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Dashboard;
\ No newline at end of file
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/FixedExpenses.jsx b/frontend/src/components/pages/FixedExpenses.jsx
new file mode 100644
index 0000000000..7c3258b402
--- /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/IncomeManagement.jsx b/frontend/src/components/pages/IncomeManagement.jsx
new file mode 100644
index 0000000000..944f7d04a1
--- /dev/null
+++ b/frontend/src/components/pages/IncomeManagement.jsx
@@ -0,0 +1,247 @@
+
+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;
+ 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 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(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/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 greater than 0.');
+ return;
+ }
+
+ if (!month) {
+ alert('Please enter a valid month.');
+ return;
+ }
+
+ try {
+ 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, month) => {
+ const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/income`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ 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(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/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 (
+ <>
+
+ StoryBudget
+
+ navigate('/dashboard')}>Back to Dashboard
+
+
+
+ Manage Income
+
+
+ {months.map((month) => (
+
+ handleDelete(month._id)}>
+
+
+ Month: {month.month}
+ Amount: SEK {month.amount}
+ Date: {month.createdAt ? new Date(month.createdAt).toLocaleDateString() : "N/A"}
+
+ ))}
+
+
+ >
+ );
+};
+
+export default IncomeManagement;
\ No newline at end of file
diff --git a/frontend/src/components/pages/Landing.jsx b/frontend/src/components/pages/Landing.jsx
new file mode 100644
index 0000000000..880c9eea52
--- /dev/null
+++ b/frontend/src/components/pages/Landing.jsx
@@ -0,0 +1,267 @@
+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};
+ 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`
+ 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);
+ z-index: 3;
+`;
+
+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; // Hide buttons on small screens
+ }
+`;
+
+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;
+
+ &.transparent {
+ background-color: inherit;
+ color: ${({ theme }) => theme.colors.white};
+ }
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colors.limeGreenLight};
+ }
+`;
+
+const StarsContainer = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ 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 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 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%, 50%, 100%{
+ transform: translateY(0); // Start position
+ }
+ 25%, 75% {
+ transform: translateY(-10px); // Slight upward movement and rotation
+ }
+ }
+`;
+
+
+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 (
+
+
+ StoryBudget
+
+ navigate('/signup')}>Sign Up
+ navigate('/login')}>Login
+
+
+
+ {stars.map((star) => (
+
+ ))}
+ {/* Single falling star */}
+
+
+
+ Welcome To StoryBudget
+ Your Finances, Your Story, Your Control.
+ navigate('/signup')}>Get Started
+
+
+
+
+
+
+ )
+};
+
+export default Landing;
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/Login.jsx b/frontend/src/components/pages/Login.jsx
new file mode 100644
index 0000000000..41769ddbc1
--- /dev/null
+++ b/frontend/src/components/pages/Login.jsx
@@ -0,0 +1,340 @@
+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('');
+ 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("Login successful");
+
+ // 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);
+ }
+ }
+
+ // 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
+
+ navigate('/signup')}>Signup
+
+
+
+
+
+
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!
+
+
+
+
+ );
+};
+
+
+export default Login;
\ 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
+
+ Logout
+
+
+ );
+};
+
+export default Navbar;
\ No newline at end of file
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
diff --git a/frontend/src/components/pages/Signup.jsx b/frontend/src/components/pages/Signup.jsx
new file mode 100644
index 0000000000..79a52aed63
--- /dev/null
+++ b/frontend/src/components/pages/Signup.jsx
@@ -0,0 +1,355 @@
+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};
+ 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 Signup = () => {
+ const [name, setName] = useState('');
+ 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/signup`, {
+ name,
+ email,
+ password
+ });
+ console.log("Signup successful");
+
+ // Store the accessToken in localStorage
+ localStorage.setItem('accessToken', response.data.response.accessToken);
+
+ // 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.');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ // 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
+
+ navigate('/login')}>Login
+
+
+
+
+
+
Create Your Account
+
+
Already have an account? Login
+
+
+
+
+ {stars.map((star) => (
+
+ ))}
+
+ Welcome to StoryBudget
+ Manage your budgets effortlessly and take control of your finances today!
+
+
+
+
+ );
+};
+
+
+export default Signup;
diff --git a/frontend/src/components/pages/SpendingsManagement.jsx b/frontend/src/components/pages/SpendingsManagement.jsx
new file mode 100644
index 0000000000..69012e155d
--- /dev/null
+++ b/frontend/src/components/pages/SpendingsManagement.jsx
@@ -0,0 +1,261 @@
+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;
+ 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 SpendingsManagementContainer = 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 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(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/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();
+
+ if (amount <= 0) {
+ alert('Please enter a valid amount greater than 0.');
+ return;
+ }
+
+ 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.');
+ }
+ };
+
+ const saveSpendingToAPI = async (category, amount) => {
+ const response = await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/spendings`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ category, amount
+ }),
+ });
+
+ 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;
+ };
+
+ const handleDelete = async (id) => {
+ const confirmDelete = window.confirm('Are you sure you want to delete this spending?');
+ if (!confirmDelete) return;
+
+ try {
+ await fetch(`${import.meta.env.VITE_REACT_APP_BACKEND_URL}/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 (
+ <>
+
+ StoryBudget
+
+ navigate('/dashboard')}>Back to Dashboard
+
+
+
+ Manage Spendings
+
+
+ {spendings.map((spending) => (
+
+ handleDelete(spending._id)}>
+
+
+ {capitalizeFirstLetter(spending.category)}
+ Amount: SEK {spending.amount}
+ Date: {new Date(spending.createdAt).toLocaleDateString()}
+
+ ))}
+
+
+ >
+ );
+};
+
+export default SpendingsManagement;
\ No newline at end of file
diff --git a/frontend/src/components/pages/VariableExpenses.jsx b/frontend/src/components/pages/VariableExpenses.jsx
new file mode 100644
index 0000000000..c74997a460
--- /dev/null
+++ b/frontend/src/components/pages/VariableExpenses.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import styled from 'styled-components';
+import { theme } from '../../styles/theme';
+import { FiHome } from 'react-icons/fi';
+import { MdOutlineMovie, MdOutlineRestaurant, MdOutlineLocalCafe } from 'react-icons/md';
+import { AiOutlineShoppingCart, AiOutlineCar, 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.fontWeightMedium};
+ }
+
+ p {
+ font-size: 1rem;
+ }
+`;
+
+const VariableExpenses = () => {
+ return (
+ <>
+
+
+
+
+
+
+ Grocery
+ SEK 2500
+
+
+
+
+
+
+
+ Shopping
+ SEK 500
+
+
+
+
+
+
+
+ Restaurants
+ SEK 800
+
+
+
+
+
+
+
+ Travel
+ SEK 500
+
+
+
+
+
+
+
+ Entertainment
+ SEK 400
+
+
+
+
+
+
+
+ Cafe
+ SEK 400
+
+
+
+ >
+ );
+};
+
+export default VariableExpenses;
\ No newline at end of file
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..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";
-import "./index.css";
+
ReactDOM.createRoot(document.getElementById("root")).render(
-
+
+
+
);
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
diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js
new file mode 100644
index 0000000000..ef527fd0a5
--- /dev/null
+++ 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
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
+}