- Planned expenses: dropdown with Edit/Delete, inline edit, clone from other years - Move Logout button from navbar to Settings danger zone - Navbar reduced to 5 buttons - Apply |money filter to all input values (no trailing .00)
266 lines
14 KiB
HTML
266 lines
14 KiB
HTML
{% load static liveview django_vite %}
|
|
<!DOCTYPE html>
|
|
<html lang="en" data-theme="light" data-room="{% liveview_room_uuid %}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
|
<title>{% block title %}Kakebo{% endblock %}</title>
|
|
<meta name="description" content="Track your expenses with the Japanese budgeting method.">
|
|
<meta property="og:title" content="Kakebo">
|
|
<meta property="og:description" content="Track your expenses with the Japanese budgeting method.">
|
|
<link rel="icon" href="{% static 'img/favicon.png' %}" type="image/png">
|
|
<link rel="manifest" href="{% static 'manifest.json' %}">
|
|
<meta name="theme-color" content="#f78ca4">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
<meta name="apple-mobile-web-app-title" content="Kakebo">
|
|
<link rel="apple-touch-icon" href="{% static 'img/icon-192.png' %}">
|
|
|
|
{# Vite CSS (Tailwind + DaisyUI + custom) #}
|
|
<link rel="stylesheet" href="{% vite_asset_url 'css/main.css' %}">
|
|
|
|
{# Chart.js #}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
|
|
{# Loading bar #}
|
|
<style>
|
|
.loading-bar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
height: 3px;
|
|
width: 0;
|
|
z-index: 9998;
|
|
background-color: var(--kakebo-pink);
|
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.2s ease-out;
|
|
}
|
|
.loading-bar--loading {
|
|
opacity: 1;
|
|
width: 75%;
|
|
transition: width 8s cubic-bezier(0.1, 0.5, 0.5, 1), opacity 0.1s ease-out;
|
|
}
|
|
.loading-bar--complete {
|
|
opacity: 1;
|
|
width: 100%;
|
|
transition: width 0.2s ease-out;
|
|
}
|
|
.loading-bar--done {
|
|
opacity: 0;
|
|
width: 100%;
|
|
transition: width 0.2s ease-out, opacity 0.3s ease-out 0.1s;
|
|
}
|
|
</style>
|
|
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
|
|
<body data-controller="page" class="min-h-screen bg-base-200">
|
|
{# Bottom navigation for mobile #}
|
|
<nav class="btm-nav z-50 bg-base-100 border-t border-base-300 sm:hidden" aria-label="Main navigation">
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/"
|
|
data-nav-url="/"
|
|
class="{% if request.resolver_match.url_name == 'dashboard' %}active text-primary{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
|
</svg>
|
|
<span class="btm-nav-label text-xs">Expense</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/week/"
|
|
data-nav-url="/week/"
|
|
class="{% if 'week' in request.path %}active text-primary{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
|
</svg>
|
|
<span class="btm-nav-label text-xs">Week</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/month/"
|
|
data-nav-url="/month/"
|
|
class="{% if 'month' in request.path %}active text-primary{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
</svg>
|
|
<span class="btm-nav-label text-xs">Month</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/year/"
|
|
data-nav-url="/year/"
|
|
class="{% if 'year' in request.path %}active text-primary{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
<span class="btm-nav-label text-xs">Year</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/settings/"
|
|
data-nav-url="/settings/"
|
|
class="{% if 'settings' in request.path %}active text-primary{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
</svg>
|
|
<span class="btm-nav-label text-xs">Settings</span>
|
|
</a>
|
|
</nav>
|
|
|
|
{# Desktop navbar #}
|
|
<nav class="navbar bg-base-100 shadow-sm hidden sm:flex" aria-label="Main navigation">
|
|
<div class="flex-1">
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/"
|
|
class="btn btn-ghost text-xl gap-2">
|
|
<img src="{% static 'img/icon.svg' %}" alt="" class="w-7 h-7">
|
|
Kakebo
|
|
</a>
|
|
</div>
|
|
<div class="flex-none gap-1">
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/"
|
|
data-nav-url="/"
|
|
class="btn btn-ghost btn-sm flex-col h-auto py-2 {% if request.resolver_match.url_name == 'dashboard' %}btn-active{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
|
</svg>
|
|
<span class="text-xs">Expense</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/week/"
|
|
data-nav-url="/week/"
|
|
class="btn btn-ghost btn-sm flex-col h-auto py-2 {% if 'week' in request.path %}btn-active{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
|
</svg>
|
|
<span class="text-xs">Week</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/month/"
|
|
data-nav-url="/month/"
|
|
class="btn btn-ghost btn-sm flex-col h-auto py-2 {% if 'month' in request.path %}btn-active{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
|
</svg>
|
|
<span class="text-xs">Month</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/year/"
|
|
data-nav-url="/year/"
|
|
class="btn btn-ghost btn-sm flex-col h-auto py-2 {% if 'year' in request.path %}btn-active{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
</svg>
|
|
<span class="text-xs">Year</span>
|
|
</a>
|
|
<a data-liveview-function="navigate"
|
|
data-action="click->page#run"
|
|
data-data-url="/settings/"
|
|
data-nav-url="/settings/"
|
|
class="btn btn-ghost btn-sm flex-col h-auto py-2 {% if 'settings' in request.path %}btn-active{% endif %}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
</svg>
|
|
<span class="text-xs">Settings</span>
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
|
|
{# Main content #}
|
|
<main id="main-content" class="pb-20 sm:pb-4">
|
|
{% if messages %}
|
|
<div class="px-4 pt-4">
|
|
{% for message in messages %}
|
|
<div class="alert alert-{% if message.tags == 'error' %}error{% else %}{{ message.tags }}{% endif %} mb-2">
|
|
<span>{{ message }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
{# Modal container for liveview CRUD #}
|
|
<div id="modal-wrapper"></div>
|
|
|
|
{# Django Liveview #}
|
|
<script src="{% static 'liveview/liveview.js' %}" defer></script>
|
|
|
|
{# Loading bar #}
|
|
<div id="loading-bar" class="loading-bar" aria-hidden="true"></div>
|
|
<script>
|
|
(function() {
|
|
const bar = document.getElementById('loading-bar');
|
|
if (!bar) return;
|
|
|
|
let completeTimer = null;
|
|
let doneTimer = null;
|
|
|
|
function startLoading() {
|
|
clearTimeout(completeTimer);
|
|
clearTimeout(doneTimer);
|
|
bar.className = 'loading-bar';
|
|
void bar.offsetWidth;
|
|
bar.className = 'loading-bar loading-bar--loading';
|
|
}
|
|
|
|
function completeLoading() {
|
|
bar.className = 'loading-bar loading-bar--complete';
|
|
completeTimer = setTimeout(function() {
|
|
bar.className = 'loading-bar loading-bar--done';
|
|
doneTimer = setTimeout(function() {
|
|
bar.className = 'loading-bar';
|
|
}, 400);
|
|
}, 200);
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
const target = e.target.closest('[data-liveview-function]');
|
|
if (target && target.dataset.liveviewFunction === 'navigate') {
|
|
startLoading();
|
|
var navUrl = target.dataset.navUrl || target.dataset.dataUrl;
|
|
if (navUrl) updateActiveNav(navUrl);
|
|
}
|
|
}, true);
|
|
|
|
function updateActiveNav(url) {
|
|
document.querySelectorAll('[data-nav-url]').forEach(function(link) {
|
|
var linkUrl = link.dataset.navUrl;
|
|
var isActive = (url === '/' && linkUrl === '/') ||
|
|
(linkUrl !== '/' && url.indexOf(linkUrl) === 0);
|
|
if (link.closest('.btm-nav')) {
|
|
link.classList.toggle('active', isActive);
|
|
link.classList.toggle('text-primary', isActive);
|
|
} else {
|
|
link.classList.toggle('btn-active', isActive);
|
|
}
|
|
});
|
|
}
|
|
|
|
const mainContent = document.getElementById('main-content');
|
|
if (mainContent) {
|
|
const observer = new MutationObserver(function() {
|
|
completeLoading();
|
|
});
|
|
observer.observe(mainContent, { childList: true, subtree: true });
|
|
}
|
|
}());
|
|
</script>
|
|
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|