diff --git a/README.md b/README.md index e69de29..c4aa112 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,5 @@ +# JAST +Welcome to JAST, the spending tracker that simply tracks your spending. Not a bunch of extra fancy stuff. It let's you enter anything you spend or get paid and then gives you some stats. + +## Requirements +The only thing you really need to run this locally is docker and docker-compose. If you want to run this without docker, all you need is uv. diff --git a/core/api.py b/core/api.py new file mode 100644 index 0000000..77fd2ac --- /dev/null +++ b/core/api.py @@ -0,0 +1,44 @@ +from ninja import NinjaAPI, Schema +from typing import List +from datetime import date +from decimal import Decimal +from core.models import Transaction, Category + +api = NinjaAPI() + +# --- Schemas (Data validation for Flutter) --- +class CategorySchema(Schema): + id: int + name: str + +class TransactionOut(Schema): + id: int + title: str + amount: Decimal + date: date + category: CategorySchema | None = None + +class TransactionIn(Schema): + title: str + amount: Decimal + date: date + category_id: int | None = None + +# --- Endpoints --- +@api.get("/transactions", response=List[TransactionOut]) +def list_transactions(request): + return Transaction.objects.select_related('category').all().order_by('-date') + +@api.post("/transactions", response=TransactionOut) +def create_transaction(request, payload: TransactionIn): + transaction = Transaction.objects.create( + title=payload.title, + amount=payload.amount, + date=payload.date, + category_id=payload.category_id + ) + return transaction + +@api.get("/categories", response=List[CategorySchema]) +def list_categories(request): + return Category.objects.all() diff --git a/core/forms.py b/core/forms.py index 789e647..552a1c5 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,11 +1,26 @@ from django import forms +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.models import User from .models import Transaction class TransactionForm(forms.ModelForm): class Meta: model = Transaction fields = ['title', 'amount', 'category', 'date'] - # This widget forces the browser to show a date picker calendar - widgets = { - 'date': forms.DateInput(attrs={'type': 'date'}), - } + + +class CustomUserCreationForm(UserCreationForm): + first_name = forms.CharField(max_length=30, required=True, label="First Name") + last_name = forms.CharField(max_length=30, required=True, label="Last Name") + + class Meta(UserCreationForm.Meta): + model = User + fields = UserCreationForm.Meta.fields + ('first_name', 'last_name') + + def save(self, commit=True): + user = super().save(commit=False) + user.first_name = self.cleaned_data['first_name'] + user.last_name = self.cleaned_data['last_name'] + if commit: + user.save() + return user diff --git a/core/migrations/0002_transaction_user.py b/core/migrations/0002_transaction_user.py new file mode 100644 index 0000000..b14d0e3 --- /dev/null +++ b/core/migrations/0002_transaction_user.py @@ -0,0 +1,22 @@ +# Generated by Django 6.1.1 on 2026-09-20 11:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='transaction', + name='user', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/core/models.py b/core/models.py index 72901d3..5ce65d6 100644 --- a/core/models.py +++ b/core/models.py @@ -4,8 +4,11 @@ from django.db import ( from django.utils import( timezone ) +from django.contrib.auth.models import User class Transaction(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + title = models.CharField(max_length=255) amount = models.DecimalField(max_digits=10, decimal_places=2) diff --git a/core/settings.py b/core/settings.py index c368d98..71e4eb3 100644 --- a/core/settings.py +++ b/core/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'core', + 'ninja', ] MIDDLEWARE = [ @@ -133,3 +134,9 @@ MAILERS = { }, } STATIC_ROOT = BASE_DIR / "staticfiles" +LOGIN_REDIRECT_URL = 'home' +LOGIN_URL = 'login' +STATIC_URL = 'static/' +STATICFILES_DIRS = [ + BASE_DIR / 'static', +] diff --git a/core/static/css/style.css b/core/static/css/style.css new file mode 100644 index 0000000..164e359 --- /dev/null +++ b/core/static/css/style.css @@ -0,0 +1,391 @@ +:root { + /* Gruvbox Dark Palette */ + --bg-main: #282828; /* bg0 */ + --bg-card: #3c3836; /* bg1 */ + --bg-card-hover: #504945; /* bg2 */ + --border: #504945; /* bg2 */ + --border-hover: #665c54; /* bg3 */ + --border-focus: #fabd2f; /* Gruvbox Yellow */ + + --text-main: #ebdbb2; /* fg1 */ + --text-heading: #fbf1c7; /* fg0 */ + --text-muted: #a89984; /* fg4 */ + + --primary: #fabd2f; /* Gruvbox Yellow */ + --primary-hover: #d79921; /* Gruvbox Dark Yellow */ + --primary-text: #282828; /* Dark contrast text for yellow buttons */ + + --income: #b8bb26; /* Gruvbox Green */ + --expense: #fb4934; /* Gruvbox Red */ + --danger: #fb4934; /* Gruvbox Red */ + --accent-blue: #83a598; /* Gruvbox Blue */ + + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 16px; + --shadow: 0 4px 20px rgba(0, 0, 0, 0.4); +} + +* { + box-sizing: border-box; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + max-width: 920px; + margin: 0 auto; + padding: 24px 20px 60px 20px; + color: var(--text-main); + background-color: var(--bg-main); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* Headings & Links */ +h1, h2, h3, h4 { + margin-top: 0; + color: var(--text-heading); + font-weight: 700; + letter-spacing: -0.02em; +} + +a { + color: var(--primary); + text-decoration: none; + transition: color 0.15s ease; +} + +a:hover { + color: var(--primary-hover); +} + +/* Navbar */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 0; + border-bottom: 1px solid var(--border); + margin-bottom: 32px; +} + +.navbar-brand { + font-size: 22px; + font-weight: 800; + letter-spacing: 0.05em; + text-decoration: none; + color: var(--primary); +} + +.navbar-actions { + display: flex; + align-items: center; + gap: 10px; +} + +/* Header Section & Balance Card */ +.header-section { + margin-bottom: 32px; + padding: 24px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow); +} + +.header-section h1 { + margin: 0 0 8px 0; + font-size: 28px; +} + +.balance-text { + margin: 0; + font-size: 15px; + color: var(--text-muted); +} + +.balance-text strong { + font-size: 24px; + color: var(--primary); + font-weight: 700; + margin-left: 6px; +} + +/* Cards & Auth Containers */ +.card { + background: var(--bg-card); + border: 1px solid var(--border); + padding: 24px; + border-radius: var(--radius-md); + margin-bottom: 32px; + box-shadow: var(--shadow); +} + +.auth-container { + max-width: 420px; + margin: 60px auto; +} + +.auth-container h2 { + text-align: center; + margin-bottom: 24px; +} + +.auth-container p { + text-align: center; + color: var(--text-muted); + font-size: 14px; +} + +/* Tables */ +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin-top: 10px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; +} + +th { + background: var(--bg-card-hover); + color: var(--text-muted); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 14px 16px; + border-bottom: 1px solid var(--border); + text-align: left; +} + +td { + padding: 14px 16px; + border-bottom: 1px solid var(--border); + font-size: 14px; + color: var(--text-main); +} + +tr:last-child td { + border-bottom: none; +} + +tr:hover td { + background-color: var(--bg-card-hover); +} + +.income { + color: var(--income); + font-weight: 600; +} + +.expense { + color: var(--expense); + font-weight: 500; +} + +/* Filter Pills */ +.filters { + margin-bottom: 32px; + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.filters a { + padding: 8px 16px; + border-radius: var(--radius-sm); + text-decoration: none; + border: 1px solid var(--border); + background: var(--bg-card); + color: var(--text-muted); + font-size: 14px; + font-weight: 500; + transition: all 0.15s ease; +} + +.filters a:hover { + border-color: var(--border-hover); + color: var(--text-main); +} + +.filters a.active { + background: var(--primary); + color: var(--primary-text); + border-color: var(--primary); + font-weight: 600; + box-shadow: 0 0 12px rgba(250, 189, 47, 0.25); +} + +/* Chart Container */ +.chart-container { + position: relative; + height: 400px; + width: 100%; + margin-top: 24px; + padding: 20px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); +} + +/* Buttons */ +button, .btn { + background: var(--primary); + color: var(--primary-text); + padding: 9px 18px; + border: none; + border-radius: var(--radius-sm); + cursor: pointer; + text-decoration: none; + font-size: 14px; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +button:hover, .btn:hover { + background: var(--primary-hover); + transform: translateY(-1px); +} + +.btn-stats, .btn-nav { + background: var(--bg-card-hover); + color: var(--text-main); + border: 1px solid var(--border-hover); + font-weight: 600; +} + +.btn-stats:hover, .btn-nav:hover { + background: var(--border-hover); + color: var(--text-heading); +} + +.btn-logout, .btn-delete { + background: rgba(251, 73, 52, 0.15); + color: var(--danger); + border: 1px solid rgba(251, 73, 52, 0.3); + font-weight: 600; +} + +.btn-logout:hover, .btn-delete:hover { + background: var(--danger); + color: var(--bg-main); +} + +.btn-edit { + background: rgba(131, 165, 152, 0.15); + color: var(--accent-blue); + border: 1px solid rgba(131, 165, 152, 0.3); + text-decoration: none; + padding: 6px 12px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 600; + margin-right: 6px; + transition: all 0.15s ease; +} + +.btn-edit:hover { + background: var(--accent-blue); + color: var(--bg-main); +} + +.cancel { + background: var(--bg-card-hover); + color: var(--text-muted); + text-decoration: none; + padding: 9px 18px; + border-radius: var(--radius-sm); + display: inline-block; + margin-left: 10px; + font-size: 14px; + font-weight: 600; + border: 1px solid var(--border-hover); + transition: all 0.15s ease; +} + +.cancel:hover { + background: var(--border-hover); + color: var(--text-main); +} + +/* Django Form Styling */ +form p { + margin-bottom: 18px; +} + +form label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +form input[type="text"], +form input[type="password"], +form input[type="email"], +form input[type="number"], +form input[type="date"], +form select { + width: 100%; + box-sizing: border-box; + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 14px; + background-color: var(--bg-main); + color: var(--text-main); + transition: all 0.15s ease; +} + +form input:focus, +form select:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 3px rgba(250, 189, 47, 0.2); +} + +/* Django Form Password Requirements & Help Text */ +.helptext, form span.helptext { + display: block; + font-size: 12px; + color: var(--text-muted); + margin-top: 6px; + line-height: 1.5; +} + +.helptext ul { + margin: 6px 0 10px 0; + padding-left: 18px; + color: var(--text-muted); + font-size: 12px; +} + +.helptext li { + margin-bottom: 4px; +} + +/* Validation Errors */ +.errorlist { + color: var(--expense); + font-size: 13px; + list-style: none; + padding: 10px 14px; + margin: 6px 0 16px 0; + background: rgba(251, 73, 52, 0.1); + border: 1px solid rgba(251, 73, 52, 0.25); + border-radius: var(--radius-sm); + font-weight: 500; +} + +.errorlist li { + margin: 2px 0; +} diff --git a/core/templates/edit.html b/core/templates/edit.html index 19b9cb1..4487f60 100644 --- a/core/templates/edit.html +++ b/core/templates/edit.html @@ -1,13 +1,11 @@ +{% load static %} - + - Edit Transaction - + + + Edit Transaction - JAST +

Editing: {{ transaction.title }}

diff --git a/core/templates/home.html b/core/templates/home.html index 18364c4..59afd59 100644 --- a/core/templates/home.html +++ b/core/templates/home.html @@ -1,25 +1,30 @@ +{% load static %} - + - DIME Tracker - + + + JAST + -

Just another spending tracker

-
-

Total Balance: {{ total_balance|floatformat:2 }} kr

- View Statistics 📊 + + + +
+

Hello, {% if user.first_name %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}!

+

Total Balance: {{ total_balance|floatformat:2 }} kr

+

Add Transaction

@@ -30,32 +35,37 @@
- - - - - - - - {% for t in transactions %} - - - - - - - - {% empty %} - - {% endfor %} + + + + + + + + + + + {% for t in transactions %} + + + + + + + + {% empty %} + + {% endfor %} +
DateTitleCategoryAmountActions
{{ t.date }}{{ t.title }}{{ t.category.name|default:"-" }} - {{ t.amount }} kr - - Edit - - {% csrf_token %} - - -
No transactions yet. Start spending!
DateTitleCategoryAmountActions
{{ t.date }}{{ t.title }}{{ t.category.name|default:"-" }} + {{ t.amount }} kr + + Edit +
+ {% csrf_token %} + +
+
No transactions yet. Start spending!
+ diff --git a/core/templates/registration/login.html b/core/templates/registration/login.html new file mode 100644 index 0000000..44f0f69 --- /dev/null +++ b/core/templates/registration/login.html @@ -0,0 +1,23 @@ +{% load static %} + + + + + + Login - JAST + + + +
+

Login to JAST

+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+

Don't have an account? Register here

+
+ + diff --git a/core/templates/registration/register.html b/core/templates/registration/register.html new file mode 100644 index 0000000..e450719 --- /dev/null +++ b/core/templates/registration/register.html @@ -0,0 +1,23 @@ +{% load static %} + + + + + + Register - JAST + + + +
+

Register for JAST

+
+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+

Already have an account? Log in here

+
+ + diff --git a/core/templates/stats.html b/core/templates/stats.html index 32e5abd..d073a5c 100644 --- a/core/templates/stats.html +++ b/core/templates/stats.html @@ -1,25 +1,30 @@ +{% load static %} - + - DIME Statistics + + + JAST Statistics - + - + + +

Spending Statistics

+
Last 7 Days Last 30 Days @@ -27,19 +32,20 @@ All Time
+