removed frontend, added ci

This commit is contained in:
Lyxminxx committed 2026-09-21 20:50:53 +02:00
1 parent b6dbb34a89
commit 90ad2f05ce
11 files changed
+69 -752

No files matched your search

+35
View File
@@ -108,6 +108,13 @@ class TransactionIn(Schema):
class SuccessResponse(Schema):
success: bool
class UserUpdateIn(Schema):
username: str | None = None
email: str | None = None
first_name: str | None = None
last_name: str | None = None
password: str | None = None
# Endpoints
@api.post("/auth/register", response=TokenOut)
@@ -145,6 +152,34 @@ def login_view(request, payload: LoginIn):
"user": user,
}
@api.put("/auth/me", response=UserOut, auth=auth)
def update_current_user(request, payload: UserUpdateIn):
user = request.auth
if payload.username and payload.username != user.username:
if User.objects.filter(username=payload.username).exclude(id=user.id).exists():
raise HttpError(400, "Username already taken")
user.username = payload.username
if payload.email is not None:
user.email = payload.email
if payload.first_name is not None:
user.first_name = payload.first_name
if payload.last_name is not None:
user.last_name = payload.last_name
if payload.password:
user.set_password(payload.password)
user.save()
return user
@api.delete("/auth/me", response=SuccessResponse, auth=auth)
def delete_current_user(request):
user = request.auth
user.delete()
return {"success": True}
@api.post("/auth/refresh", response=RefreshOut)
def refresh_token_view(request, payload: RefreshIn):
-26
View File
@@ -1,26 +0,0 @@
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']
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
-391
View File
@@ -1,391 +0,0 @@
: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;
}
-21
View File
@@ -1,21 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Transaction - JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<h2>Editing: {{ transaction.title }}</h2>
<div class="card">
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save Changes</button>
<a href="{% url 'home' %}" class="cancel">Cancel</a>
</form>
</div>
</body>
</html>
-71
View File
@@ -1,71 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<nav class="navbar">
<a href="{% url 'home' %}" class="navbar-brand">JAST</a>
<div class="navbar-actions">
<a href="{% url 'stats' %}" class="btn btn-stats">View Statistics 📊</a>
<form action="{% url 'logout' %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to log out?');">
{% csrf_token %}
<button type="submit" class="btn btn-logout">Logout</button>
</form>
</div>
</nav>
<div class="header-section">
<h1>Hello, {% if user.first_name %}{{ user.first_name }} {{ user.last_name }}{% else %}{{ user.username }}{% endif %}!</h1>
<p class="balance-text">Total Balance: <strong>{{ total_balance|floatformat:2 }} kr</strong></p>
</div>
<div class="card">
<h3>Add Transaction</h3>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save Transaction</button>
</form>
</div>
<table>
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Category</th>
<th>Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for t in transactions %}
<tr>
<td>{{ t.date }}</td>
<td>{{ t.title }}</td>
<td>{{ t.category.name|default:"-" }}</td>
<td class="{% if t.is_income %}income{% else %}expense{% endif %}">
{{ t.amount }} kr
</td>
<td>
<a href="{% url 'edit_transaction' t.pk %}" class="btn-edit">Edit</a>
<form action="{% url 'delete_transaction' t.pk %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this transaction?');">
{% csrf_token %}
<button type="submit" class="btn-delete">Delete</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="5">No transactions yet. Start spending!</td></tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
-23
View File
@@ -1,23 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<div class="auth-container">
<h2>Login to JAST</h2>
<div class="card">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Login</button>
</form>
</div>
<p>Don't have an account? <a href="{% url 'register' %}">Register here</a></p>
</div>
</body>
</html>
-23
View File
@@ -1,23 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - JAST</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<div class="auth-container">
<h2>Register for JAST</h2>
<div class="card">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Sign Up</button>
</form>
</div>
<p>Already have an account? <a href="{% url 'login' %}">Log in here</a></p>
</div>
</body>
</html>
-91
View File
@@ -1,91 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAST Statistics</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<!-- Top Navbar -->
<nav class="navbar">
<a href="{% url 'home' %}" class="navbar-brand">JAST</a>
<div class="navbar-actions">
<a href="{% url 'home' %}" class="btn btn-nav">← Back to Dashboard</a>
<form action="{% url 'logout' %}" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to log out?');">
{% csrf_token %}
<button type="submit" class="btn btn-logout">Logout</button>
</form>
</div>
</nav>
<h1>Spending Statistics</h1>
<!-- Time Period Filters -->
<div class="filters">
<a href="?filter=week" class="{% if current_filter == 'week' %}active{% endif %}">Last 7 Days</a>
<a href="?filter=month" class="{% if current_filter == 'month' %}active{% endif %}">Last 30 Days</a>
<a href="?filter=year" class="{% if current_filter == 'year' %}active{% endif %}">Last 365 Days</a>
<a href="?filter=all" class="{% if current_filter == 'all' %}active{% endif %}">All Time</a>
</div>
<!-- Chart Canvas -->
<div class="chart-container">
<canvas id="categoryChart"></canvas>
</div>
<script>
const labels = JSON.parse('{{ labels|safe }}');
const data = JSON.parse('{{ data|safe }}');
const ctx = document.getElementById('categoryChart').getContext('2d');
if (data.length === 0) {
ctx.font = "16px system-ui, sans-serif";
ctx.fillStyle = "#a89984"; // Gruvbox muted text
ctx.fillText("No expenses logged for this time period.", 10, 50);
} else {
new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
label: 'Amount Spent (kr)',
data: data,
backgroundColor: [
'#fabd2f', // Gruvbox Yellow
'#fe8019', // Gruvbox Orange
'#fb4934', // Gruvbox Red
'#b8bb26', // Gruvbox Green
'#83a598', // Gruvbox Blue
'#d3869b', // Gruvbox Purple
'#8ec07c' // Gruvbox Aqua
],
borderColor: '#3c3836', // Card background color for clean slice gaps
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
color: '#ebdbb2', // Gruvbox primary text
font: {
family: 'system-ui, -apple-system, sans-serif',
size: 13
}
}
}
}
}
});
}
</script>
</body>
</html>
-13
View File
@@ -1,24 +1,11 @@
from django.contrib import admin
from django.urls import path
from django.contrib.auth import views as auth_views
from core import views
from core.api import api # Import your Django Ninja API instance
urlpatterns = [
# Admin Panel
path('admin/', admin.site.urls),
# Web App Views
path('', views.home, name='home'),
path('edit/<int:pk>/', views.edit_transaction, name='edit_transaction'),
path('delete/<int:pk>/', views.delete_transaction, name='delete_transaction'),
path('stats/', views.stats, name='stats'),
# Auth Views
path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
path('register/', views.register, name='register'),
# Django Ninja API
path('api/', api.urls),
]
-93
View File
@@ -1,93 +0,0 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Sum
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login
from .models import Transaction
from .forms import TransactionForm, CustomUserCreationForm
import json
from datetime import timedelta
from django.utils import timezone
def register(request):
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('home')
else:
form = CustomUserCreationForm()
return render(request, 'registration/register.html', {'form': form})
@login_required
def home(request):
if request.method == 'POST':
form = TransactionForm(request.POST)
if form.is_valid():
transaction = form.save(commit=False)
transaction.user = request.user
transaction.save()
return redirect('home')
else:
form = TransactionForm()
transactions = Transaction.objects.filter(user=request.user)
total_balance = transactions.aggregate(Sum('amount'))['amount__sum'] or 0
context = {
'form': form,
'transactions': transactions,
'total_balance': total_balance,
}
return render(request, 'home.html', context)
@login_required
def edit_transaction(request, pk):
transaction = get_object_or_404(Transaction, pk=pk, user=request.user)
if request.method == 'POST':
form = TransactionForm(request.POST, instance=transaction)
if form.is_valid():
form.save()
return redirect('home')
else:
form = TransactionForm(instance=transaction)
return render(request, 'edit.html', {'form': form, 'transaction': transaction})
@login_required
def delete_transaction(request, pk):
transaction = get_object_or_404(Transaction, pk=pk, user=request.user)
if request.method == 'POST':
transaction.delete()
return redirect('home')
@login_required
def stats(request):
time_filter = request.GET.get('filter', 'all')
now = timezone.now().date()
expenses = Transaction.objects.filter(user=request.user, amount__lt=0)
if time_filter == 'week':
expenses = expenses.filter(date__gte=now - timedelta(days=7))
elif time_filter == 'month':
expenses = expenses.filter(date__gte=now - timedelta(days=30))
elif time_filter == 'year':
expenses = expenses.filter(date__gte=now - timedelta(days=365))
category_data = expenses.values('category__name').annotate(total=Sum('amount'))
labels = []
data = []
for item in category_data:
cat_name = item['category__name'] or 'Uncategorized'
labels.append(cat_name)
data.append(abs(float(item['total'])))
context = {
'labels': json.dumps(labels),
'data': json.dumps(data),
'current_filter': time_filter,
}
return render(request, 'stats.html', context)