This commit is contained in:
Elias Jansson
2024-10-20 09:03:17 +02:00
parent 4d3f340602
commit ff90b86fd2
10 changed files with 700 additions and 244 deletions

View File

@@ -1,10 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Aberwyn.Data; // Adjust based on your project namespace
using Aberwyn.Models; // Adjust based on your project namespace
using Aberwyn.Data;
using Aberwyn.Models;
using System.Collections.Generic;
using System.Globalization; // For month name indexing
namespace Aberwyn.Controllers // Adjust namespace based on your project
namespace Aberwyn.Controllers
{
[Route("api/[controller]")]
[ApiController]
@@ -18,23 +17,58 @@ namespace Aberwyn.Controllers // Adjust namespace based on your project
}
[HttpGet("items")]
public IActionResult GetBudgetItems(string month = null, int? year = null)
public IActionResult GetBudgetItems([FromQuery] int month, [FromQuery] int year)
{
// Fetch all budget items from the service
var items = _budgetService.GetBudgetItems();
// Filter the items based on the provided month and year
if (!string.IsNullOrEmpty(month) && year.HasValue)
{
// Convert month name to month number (1 for January, 2 for February, etc.)
int monthNumber = Array.IndexOf(CultureInfo.CurrentCulture.DateTimeFormat.MonthNames, month) + 1;
// Filter items where the month and year match
items = items.Where(item => item.Month == monthNumber && item.Year == year.Value).ToList();
}
// Return the filtered (or unfiltered) list as JSON
var items = _budgetService.GetBudgetItems(month, year);
return Ok(items);
}
// New endpoint to get categories
[HttpGet("categories")]
public IActionResult GetCategories()
{
var categories = _budgetService.GetCategories();
return Ok(categories);
}
[HttpPut("items")]
public IActionResult UpdateBudgetItem([FromBody] BudgetItem item)
{
if (item == null || item.ID <= 0)
{
return BadRequest("Invalid budget item data.");
}
// Assuming you have a method in your BudgetService to update an item
var result = _budgetService.UpdateBudgetItem(item);
if (result)
{
return Ok("Item updated successfully.");
}
return StatusCode(500, "Error updating item.");
}
[HttpPost("items")]
public IActionResult AddBudgetItem([FromBody] BudgetItem item)
{
if (item == null || string.IsNullOrEmpty(item.Name) || item.Amount <= 0)
{
return BadRequest("Invalid budget item data.");
}
// Assuming you have a method in your BudgetService to add an item
var result = _budgetService.AddBudgetItem(item);
if (result)
{
return CreatedAtAction(nameof(GetBudgetItems), new { id = item.ID }, item);
}
return StatusCode(500, "Error adding item.");
}
}
}

View File

@@ -29,32 +29,35 @@ namespace Aberwyn.Controllers
return View();
}
// Action method to handle budget requests
public IActionResult RealEstate()
{
return View();
}
// Optimized Budget Action to fetch filtered data directly from the database
public IActionResult Budget(string month, int? year)
{
// Load all budget items from the database
var allItems = _budgetService.GetBudgetItems();
// Default to current month and year if parameters are not provided
int selectedMonth = !string.IsNullOrEmpty(month)
? Array.IndexOf(CultureInfo.CurrentCulture.DateTimeFormat.MonthNames, month) + 1
: DateTime.Now.Month;
// Filter items by selected month and year (if provided)
if (!string.IsNullOrEmpty(month) && year.HasValue)
int selectedYear = year ?? DateTime.Now.Year;
// Fetch budget items for the selected month and year directly from the database
var budgetItems = _budgetService.GetBudgetItems(selectedMonth, selectedYear);
// Create the BudgetModel
var budgetModel = new BudgetModel
{
int monthNumber = Array.IndexOf(
CultureInfo.CurrentCulture.DateTimeFormat.MonthNames, month) + 1;
BudgetItems = budgetItems.ToList() // Ensure this is a list
};
allItems = allItems
.Where(item => item.Month == monthNumber && item.Year == year.Value)
.ToList();
}
// Group items by category
var groupedItems = allItems
.GroupBy(item => item.Category ?? "Uncategorized") // Handle null categories
.ToDictionary(g => g.Key, g => g.ToList());
// Pass the grouped items to the view
return View(groupedItems);
// Pass the BudgetModel to the view
return View(budgetModel);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{

View File

@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using System.Threading.Tasks;
[Route("api/[controller]")]
[ApiController]
public class RealEstateApiController : ControllerBase
{
private readonly HttpClient _httpClient;
public RealEstateApiController(HttpClient httpClient)
{
_httpClient = httpClient;
}
[HttpPost("download")]
public async Task<IActionResult> DownloadWebsite([FromBody] WebsiteRequest request)
{
if (string.IsNullOrWhiteSpace(request.Url))
{
return BadRequest("URL cannot be empty.");
}
try
{
var response = await _httpClient.GetAsync(request.Url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
// Optional: Save content to a file
System.IO.File.WriteAllText("DownloadedWebsite.html", content);
return Ok("Website downloaded successfully.");
}
catch (HttpRequestException ex)
{
return StatusCode(500, $"Error downloading website: {ex.Message}");
}
}
}
public class WebsiteRequest
{
public string Url { get; set; }
}

View File

@@ -1,10 +1,9 @@
namespace Aberwyn.Data
{
using MySql.Data.MySqlClient; // Make sure you have this package installed
using System.Collections.Generic;
using Microsoft.Extensions.Configuration; // Add this for IConfiguration
using Aberwyn.Models; // Adjust the namespace
using MySql.Data.MySqlClient;
using System.Collections.Generic;
using Aberwyn.Models;
namespace Aberwyn.Data
{
public class BudgetService
{
private readonly IConfiguration _configuration;
@@ -14,42 +13,130 @@
_configuration = configuration;
}
public List<BudgetItem> GetBudgetItems()
public MySqlConnection GetConnection()
{
List<BudgetItem> budgetItems = new List<BudgetItem>();
string connectionString = _configuration.GetConnectionString("DefaultConnection"); // Use connection string from config
using (var connection = new MySqlConnection(connectionString))
var connectionString = _configuration.GetConnectionString("DefaultConnection");
return new MySqlConnection(connectionString);
}
public bool UpdateBudgetItem(BudgetItem item)
{
using (var connection = GetConnection())
{
connection.Open();
string query = @"
SELECT b.Name, b.Amount, b.Month, b.Year,
c1.Name AS Category, c2.Name AS SubCategoryName
FROM tblBudgetItems b
LEFT JOIN tblCategories c1 ON b.Category = c1.idtblCategories
LEFT JOIN tblCategories c2 ON b.SubCategory = c2.idtblCategories";
using (MySqlCommand cmd = new MySqlCommand(query, connection))
string query = @"
UPDATE tblBudgetItems
SET Name = @name, Amount = @amount
WHERE idtblBudgetItems = @id";
using (var cmd = new MySqlCommand(query, connection))
{
using (MySqlDataReader reader = cmd.ExecuteReader())
cmd.Parameters.AddWithValue("@name", item.Name);
cmd.Parameters.AddWithValue("@amount", item.Amount);
cmd.Parameters.AddWithValue("@id", item.ID);
return cmd.ExecuteNonQuery() > 0; // Returns true if one or more rows are updated
}
}
}
public List<BudgetItem> GetBudgetItems(int month, int year)
{
var budgetItems = new List<BudgetItem>();
using (var connection = GetConnection())
{
connection.Open();
string query = @"
SELECT
b.idtblBudgetItems AS id,
b.Name AS item_name,
b.Amount AS amount,
c1.Name AS category,
b.Month,
b.Year,
b.Description AS description
FROM tblBudgetItems b
LEFT JOIN tblCategories c1 ON b.Category = c1.idtblCategories
WHERE b.Month = @month AND b.Year = @year";
using (var cmd = new MySqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@month", month);
cmd.Parameters.AddWithValue("@year", year);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
budgetItems.Add(new BudgetItem
{
Name = reader.GetString("Name"),
Amount = reader.GetDecimal("Amount"), // Changed to GetDecimal for Amount
ID = reader.GetInt32("id"),
Name = reader.GetString("item_name"), // Updated alias
Amount = reader.GetDecimal("amount"),
Category = reader.GetString("category"),
Month = reader.GetInt32("Month"),
Year = reader.GetInt32("Year"),
Category = reader.IsDBNull(reader.GetOrdinal("Category")) ? null : reader.GetString("Category"), // Retrieve as string
SubCategory = reader.IsDBNull(reader.GetOrdinal("SubCategoryName")) ? null : reader.GetString("SubCategoryName") // SubCategory name
Description = reader.IsDBNull(reader.GetOrdinal("description"))
? null
: reader.GetString("description")
});
}
}
}
}
return budgetItems; // Return the list of budget items
return budgetItems;
}
public bool AddBudgetItem(BudgetItem item)
{
using (var connection = GetConnection())
{
connection.Open();
string query = @"
INSERT INTO tblBudgetItems (Name, Amount, Category, Month, Year)
VALUES (@name, @amount, @category, @month, @year)";
using (var cmd = new MySqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@name", item.Name);
cmd.Parameters.AddWithValue("@amount", item.Amount);
cmd.Parameters.AddWithValue("@category", item.Category);
cmd.Parameters.AddWithValue("@month", item.Month);
cmd.Parameters.AddWithValue("@year", item.Year);
return cmd.ExecuteNonQuery() > 0; // Returns true if a row was inserted
}
}
}
// New method to fetch all categories
public List<string> GetCategories()
{
var categories = new List<string>();
using (var connection = GetConnection())
{
connection.Open();
string query = "SELECT Name FROM tblCategories"; // Adjust based on your table structure
using (var cmd = new MySqlCommand(query, connection))
{
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
categories.Add(reader.GetString("Name"));
}
}
}
}
return categories;
}
}
}

View File

@@ -2,8 +2,9 @@
{
public class BudgetModel
{
public List<BudgetItem> BudgetItems { get; set; }
public List<BudgetItem> BudgetItems { get; set; } = new List<BudgetItem>(); // Initialize with an empty list
}
public class BudgetItem
{
public int ID { get; set; }

View File

@@ -7,6 +7,8 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages(); // Add this line to enable Razor Pages
builder.Services.AddHttpClient(); // Register HttpClient
// Configure your DbContext with MySQL
builder.Services.AddDbContext<BudgetContext>(options =>

View File

@@ -1,151 +1,115 @@
<!DOCTYPE html>
@model Aberwyn.Models.BudgetModel
@{
Layout = "_Layout"; // Assuming you have a layout file named _Layout.cshtml
}
<!DOCTYPE html>
<html lang="en" ng-app="budgetApp">
<head>
<meta charset="utf-8" />
<meta charset="utf-8">
<title>Budget Overview</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<style>
.spreadsheet-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: center;
vertical-align: middle;
min-width: 150px;
}
th {
background-color: #f1f1f1;
font-weight: bold;
}
tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
.item-name {
font-weight: bold;
}
.item-amount {
color: #007bff;
}
</style>
<link rel="stylesheet" type="text/css" href="~/css/site.css">
</head>
<body ng-controller="BudgetController">
<h1>Budget Overview</h1>
<div class="budget-page">
<h1>Budget Overview</h1>
<!-- Month and Year Dropdowns -->
<form ng-submit="filterBudget()">
<label for="month">Month:</label>
<select id="month" ng-model="selectedMonth">
<option ng-repeat="month in months" value="{{ month }}">{{ month }}</option>
</select>
<form ng-submit="filterBudget()">
<label for="month">Month:</label>
<select id="month" ng-model="selectedMonth">
<option ng-repeat="month in months" value="{{ month }}">{{ month }}</option>
</select>
<label for="year">Year:</label>
<select id="year" ng-model="selectedYear">
<option ng-repeat="year in years" value="{{ year }}">{{ year }}</option>
</select>
<label for="year">Year:</label>
<select id="year" ng-model="selectedYear">
<option ng-repeat="year in years" value="{{ year }}">{{ year }}</option>
</select>
<button type="submit">Filter</button>
</form>
<button type="submit">Filter</button>
</form>
<table class="spreadsheet-table">
<thead>
<tr>
<th>Category</th>
<th ng-repeat="i in [].constructor(maxRows()) track by $index">Item Name {{ $index + 1 }}</th>
<th ng-repeat="i in [].constructor(maxRows()) track by $index">Amount {{ $index + 1 }}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="category in categories">
<td><strong>{{ category }}</strong></td>
<td ng-repeat="i in [].constructor(maxRows()) track by $index">
<span class="item-name">{{ getItemByCategory(category, $index) ? getItemByCategory(category, $index).name : '-' }}</span>
</td>
<td ng-repeat="i in [].constructor(maxRows()) track by $index">
<span class="item-amount">{{ getItemByCategory(category, $index) && getItemByCategory(category, $index).amount !== 0 ? getItemByCategory(category, $index).amount : '-' }}</span>
</td>
</tr>
<tr>
<td>Total</td>
<td ng-repeat="category in categories">
{{ sumForCategory(category) }}
</td>
</tr>
</tbody>
</table>
<div class="budget-container">
<div ng-repeat="category in categories" class="category-block">
<div class="category-header">
{{ category }}
<span class="total">Total: {{ sumForCategory(category) }}</span> <!-- Total moved to the header -->
</div>
<div class="items-wrapper">
<div ng-repeat="item in getItemsByCategory(category)" class="item">
<div class="item-name">
<input type="text" ng-model="item.name" ng-blur="updateItem(item)" placeholder="Name" />
</div>
<div class="item-amount">
<input type="number" ng-model="item.amount" ng-blur="updateItem(item)" placeholder="Amount" />
</div>
</div>
</div>
<div class="add-item" ng-click="addNewItem(category)">+</div> <!-- Button for adding items -->
</div>
</div>
</div>
<script>
angular.module('budgetApp', [])
.controller('BudgetController', function($scope, $http) {
.controller('BudgetController', function ($scope, $http) {
const today = new Date();
// Set initial values
$scope.months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// Initialize months and years
$scope.months = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
$scope.years = [...Array(11).keys()].map(i => today.getFullYear() - 10 + i);
$scope.categories = ['House', 'Bills', 'Services', 'Unplanned', 'Car', 'Planned', 'Save'];
// Default to current month and year
$scope.selectedMonth = $scope.months[today.getMonth()];
$scope.selectedMonth = (today.getMonth() + 1).toString();
$scope.selectedYear = today.getFullYear();
$scope.categories = [];
$scope.budgetItems = @Html.Raw(Json.Serialize(Model.BudgetItems)); // Initialize budgetItems with server-side data
$scope.budgetItems = [];
$scope.loadCategories = function () {
$http.get('/api/budgetapi/categories').then(response => {
$scope.categories = response.data;
}).catch(error => console.error("Error fetching categories:", error));
};
// Load budget items on page load
loadBudgetItems();
function loadBudgetItems() {
$http.get('/api/budgetapi/items').then(function(response) {
$scope.budgetItems = response.data;
console.log("Initial items:", response.data); // Log initial items
}).catch(function(error) {
console.error("Error loading budget items:", error);
});
}
// Filter budget items
$scope.filterBudget = function() {
$scope.filterBudget = function () {
$http.get('/api/budgetapi/items', {
params: {
month: $scope.selectedMonth,
year: $scope.selectedYear
}
}).then(function(response) {
params: { month: $scope.selectedMonth, year: $scope.selectedYear }
}).then(response => {
$scope.budgetItems = response.data;
console.log("Filtered items:", response.data); // Log filtered items
}).catch(function(error) {
console.error("Error fetching budget items:", error);
});
}).catch(error => console.error("Error fetching budget items:", error));
};
// Get item by category and index
$scope.getItemByCategory = function(category, index) {
const items = $scope.budgetItems.filter(item => item.category === category);
return items[index] || null; // Return null if no item exists
$scope.getItemsByCategory = function (category) {
return $scope.budgetItems.filter(item => item.category === category);
};
// Calculate the maximum number of rows needed
$scope.maxRows = function() {
return Math.max(...$scope.categories.map(category =>
$scope.budgetItems.filter(item => item.category === category).length
));
};
// Function to sum amounts for each category
$scope.sumForCategory = function(category) {
$scope.sumForCategory = function (category) {
return $scope.budgetItems
.filter(item => item.category === category)
.reduce((total, item) => total + item.amount, 0);
};
$scope.updateItem = function (item) {
$http.put('/api/budgetapi/items', item)
.then(response => console.log("Item updated:", response.data))
.catch(error => console.error("Error updating item:", error));
};
$scope.addNewItem = function (category) {
const newItem = {
name: '',
amount: 0,
category: category,
month: $scope.selectedMonth,
year: $scope.selectedYear
};
$scope.budgetItems.push(newItem);
};
$scope.loadCategories();
$scope.filterBudget();
});
</script>

View File

@@ -0,0 +1,38 @@
@{
ViewData["Title"] = "Real Estate Downloader";
}
<h2>Real Estate Website Downloader</h2>
<form method="post" ng-submit="downloadWebsite()">
<label for="urlInput">Enter Website URL:</label>
<input type="text" id="urlInput" ng-model="websiteUrl" placeholder="https://example.com" required />
<button type="submit">Download</button>
</form>
<div ng-if="downloadStatus">
<p>{{ downloadStatus }}</p>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script>
angular.module('realEstateApp', [])
.controller('RealEstateController', function ($scope, $http) {
$scope.websiteUrl = '';
$scope.downloadStatus = '';
$scope.downloadWebsite = function () {
if ($scope.websiteUrl) {
$http.post('/api/realestate/download', { url: $scope.websiteUrl })
.then(function (response) {
$scope.downloadStatus = 'Download successful!';
console.log(response.data); // Optional: Handle the downloaded data
})
.catch(function (error) {
$scope.downloadStatus = 'Error downloading website.';
console.error(error);
});
}
};
});
</script>

View File

@@ -1,50 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Aberwyn</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/Aberwyn.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Aberwyn</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
@using Aberwyn.Models
@model Aberwyn.Models.BudgetModel
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2024 - Aberwyn - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<title>Your Page Title</title>
</head>
<header class="header">
<div class="header-container">
<h1 class="family-header">
<span class="initial L">L</span><span class="name">ouise</span>
<span class="initial E">E</span><span class="name">lias</span>
<span class="initial W">W</span><span class="name">illiam</span>
<span class="initial E2">E</span><span class="name">lin</span>
<span class="initial L2">L</span><span class="name">udwig</span>
</h1>
</div>
</header>
<body>
<div class="grid-container">
<aside class="left-sidebar"> <!-- Left sidebar -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Home" asp-action="Index">
<i class="fas fa-home"></i> Home
</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Home" asp-action="Budget">
<i class="fas fa-wallet"></i> Budget
</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Home" asp-action="RealEstate">
<i class="fas fa-building"></i> RealEstate
</a>
</li>
</ul>
</aside>
<main role="main" class="main-content"> <!-- Main content area -->
@RenderBody() <!-- This is where the content of each page will be rendered -->
</main>
<aside class="right-sidebar"> <!-- Updated Right Sidebar -->
@if (Model != null && Model.BudgetItems != null)
{
<h3 class="sidebar-budget-header">Budget this month: <span class="total-budget-amount">@Model.BudgetItems.Sum(item => item.Amount)</span></h3>
<div class="sidebar-budget-container"> <!-- New Container -->
@if (Model.BudgetItems.Any())
{
foreach (var categoryGroup in Model.BudgetItems.GroupBy(item => item.Category))
{
<div class="sidebar-budget-item">
<span class="budget-item-name">@categoryGroup.Key</span>
<span class="budget-item-amount">@categoryGroup.Sum(item => item.Amount)</span>
</div>
}
}
else
{
<p>No categories available.</p>
}
</div>
}
else
{
<h3>No budget data available.</h3>
}
</aside>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const budgetHeader = document.querySelector('.sidebar-budget-header');
const budgetContainer = document.querySelector('.sidebar-budget-container');
budgetHeader.addEventListener('click', function () {
budgetContainer.style.display = budgetContainer.style.display === 'none' ? 'block' : 'none';
});
// Initially hide the budget items container
budgetContainer.style.display = 'none';
});
</script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@@ -1,18 +1,257 @@
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
html {
position: relative;
min-height: 100%;
}
/* Base Styles */
body {
margin-bottom: 60px;
}
background-color: #1F2C3C;
color: #4b004b;
font-family: 'Roboto', sans-serif;
margin: 0;
padding: 20px; /* Keep padding for general layout */
}
/* Container for Centering and Max Width */
.budget-page {
width: 100%; /* Make the page full width */
max-width: 1900px; /* You can adjust or remove this */
margin: 0 auto; /* Centering */
padding: 0; /* Removed extra padding for budget page */
}
/* Grid Layout */
.grid-container {
display: grid;
grid-template-columns: 250px 1fr 250px;
gap: 10px; /* Keep existing gap */
padding: 0; /* Removed padding for grid container */
margin: 0; /* Removed margin for grid container */
}
.left-sidebar, .sidebar-budget-right {
background-color: #f4f4f4;
border-radius: 12px;
padding: 10px; /* Adjusted padding for sidebars */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.main-content {
padding: 10px;
background-color: #f4f4f4; /* Light gray instead of white */
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.nav-link {
color: #4b004b;
}
.nav-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: 1fr; /* Responsive layout */
}
}
/* Budget Container */
.budget-container {
display: flex;
flex-wrap: wrap;
gap: 10px; /* Reduced gap for tighter layout */
padding: 0; /* Removed padding */
margin: 0; /* Removed margin */
}
/* Category Block */
.category-block {
flex: 1 1 300px;
max-width: 230px;
min-width: 180px;
margin: 0; /* Removed margin */
}
.category-header {
display: flex;
justify-content: space-between; /* Aligns the category name and total */
align-items: center; /* Center vertically */
padding: 5px; /* Retain padding */
background-color: #6a0dad; /* Adjust color as needed */
color: #ffffff;
border-radius: 8px;
margin-bottom: 10px;
font-weight: bold;
max-width: 100%; /* Ensures the header doesn't exceed the width of its container */
box-sizing: border-box; /* Include padding in width calculation */
}
.total {
font-weight: bold;
color: #ffffff; /* Keep white for visibility */
background-color: rgba(106, 13, 173, 0.7); /* Add a semi-transparent background for contrast */
padding: 5px 10px; /* Add some padding for spacing */
border-radius: 5px; /* Rounded corners */
}
/* Items Layout */
.items-wrapper {
display: flex;
min-width: 200px;
flex-direction: column;
padding: 0; /* Removed padding */
margin: 0; /* Removed margin */
}
/* Item Styles */
.item {
display: flex;
align-items: center;
margin-bottom: 5px; /* Space between items */
gap: 5px; /* Reduced gap between item components */
}
/* Input Styles */
.item-name input {
flex: 1; /* Compact size for amount field */
max-width: 20ch; /* Allow up to 7 characters */
padding: 6px; /* Slightly reduced padding */
border: none;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
background-color: #f9f9f9;
transition: background-color 0.3s;
}
.item-amount input {
flex: 1; /* Compact size for amount field */
max-width: 7ch; /* Allow up to 7 characters */
padding: 6px; /* Slightly reduced padding */
border: none;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
background-color: #f9f9f9;
transition: background-color 0.3s;
}
/* Focus Effect */
.item-name input:focus,
.item-amount input:focus {
background-color: #e6e6e6;
}
/* Add Item Button */
.add-item {
display: flex; /* Use flex to center the "+" */
align-items: center; /* Center the "+" vertically */
justify-content: center; /* Center the "+" horizontally */
height: 25px; /* Adjusted height to match input fields */
cursor: pointer;
color: #ffffff; /* White text color for contrast */
background-color: #6a0dad; /* Use the same color as the header for consistency */
margin-top: 5px; /* Reduced space above the button */
border: none; /* No border */
border-radius: 5px; /* Rounded corners */
font-size: 15px; /* Adjusted font size for better fit */
transition: background-color 0.3s, transform 0.3s; /* Transition for hover effect */
}
/* Hover Effect for Add Item Button */
.add-item:hover {
background-color: #5a0c9a; /* Darker shade on hover */
transform: scale(1.05); /* Slightly enlarge on hover for emphasis */
}
.header-container {
display: flex;
justify-content: flex-start; /* Aligns the header to the left */
align-items: center; /* Centers items vertically */
margin-bottom: 0px; /* Adjust space below the header as needed */
}
.family-header {
text-align: left; /* Ensures text aligns to the left */
margin: 0; /* Removes default margin */
}
.initial {
font-weight: bold; /* Adjust as needed */
font-size: 1.2em; /* Adjust size for initials */
}
.name {
color: #FFFFFF; /* White for names */
font-size: 0.4em; /* Adjust size as needed */
}
/* Specific colors for initials */
.initial.L {
color: #FF0000; /* Red for first L */
}
.initial.E {
color: #FF7F00; /* Orange for first E */
}
.initial.W {
color: #FFFF00; /* Yellow */
}
.initial.E2 { /* Second E */
color: #00FF00; /* Green */
}
.initial.L2 { /* Second L */
color: #0088CC; /* Blue */
}
.navbar-nav {
list-style-type: none; /* Remove bullet points */
padding: 0; /* Remove padding */
}
.nav-item {
margin: 10px 0; /* Adjust spacing between links */
}
.nav-link {
text-decoration: none; /* Remove underline */
color: #000; /* Link color */
display: flex; /* Use flexbox to align icon and text */
align-items: center; /* Center items vertically */
}
.nav-link i {
margin-right: 8px; /* Space between icon and text */
font-size: 1.2em; /* Adjust icon size */
}
/* Sidebar Right Styles */
.right-sidebar {
background-color: #f8f9fa; /* Use your existing background color */
padding: 10px; /* Reduce padding for compactness */
margin: 0; /* Remove any margins */
border-radius: 8px; /* Optional: Add rounded corners */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Optional: Add shadow for depth */
}
.sidebar-budget-header {
cursor: pointer; /* Indicate clickable header */
font-weight: bold; /* Make header bold */
margin: 0; /* Remove default margin */
padding-bottom: 5px; /* Space below header */
}
.sidebar-budget-container {
margin-top: 10px; /* Space above budget items */
}
.sidebar-budget-item {
display: flex;
justify-content: space-between; /* Space out name and amount */
padding: 5px 0; /* Reduced padding for compactness */
border-bottom: 1px solid #e0e0e0; /* Optional: Divider line */
}
.sidebar-budget-item:last-child {
border-bottom: none; /* Remove border from last item */
}