Redesign BP tracker with lightweight views and unobtrusive save confirmations
This commit is contained in:
+47
-295
@@ -1,295 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}BP Tracker{% endblock %}</title>
|
||||
<link rel="icon" type="image/svg+xml" sizes="any" href="{{ url_for('static', filename='images/favicon.svg') }}">
|
||||
<link href="/static/css/tailwind.css" rel="stylesheet">
|
||||
<script src="{{ url_for('static', filename='js/diy-turbo.min.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 text-gray-800 font-sans antialiased">
|
||||
<nav id="mobile-nav"
|
||||
class="flex items-center justify-between flex-wrap p-6 fixed w-full z-10 top-0 transition-colors duration-300 shadow-md bg-primary-800">
|
||||
<!--Logo etc-->
|
||||
<div class="flex items-center flex-shrink-0 text-white mr-6">
|
||||
<a class="text-white no-underline hover:text-white hover:no-underline" href="/">
|
||||
<span class="text-2xl pl-2"><i class="em em-grinning"></i> Blood Pressure</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!--Toggle button (hidden on large screens)-->
|
||||
<button id="mobile-menu-btn" type="button"
|
||||
class="block lg:hidden px-2 text-gray-500 hover:text-white focus:outline-none focus:text-white transition">
|
||||
<svg class="h-6 w-6 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path id="icon-close" class="hidden" fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M18.278 16.864a1 1 0 0 1-1.414 1.414l-4.829-4.828-4.828 4.828a1 1 0 0 1-1.414-1.414l4.828-4.829-4.828-4.828a1 1 0 0 1 1.414-1.414l4.829 4.828 4.828-4.828a1 1 0 1 1 1.414 1.414l-4.828 4.829 4.828 4.828z" />
|
||||
<path id="icon-menu" fill-rule="evenodd"
|
||||
d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!--Menu-->
|
||||
<div id="mobile-menu" class="w-full flex-grow lg:flex lg:items-center lg:w-auto hidden shadow-3xl">
|
||||
<ul class="pt-6 lg:pt-0 list-reset lg:flex justify-end flex-1 items-center">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="mr-3">
|
||||
<a class="inline-block py-2 px-4 no-underline
|
||||
{% if request.path == url_for('main.dashboard') %}
|
||||
text-white
|
||||
{% else %}
|
||||
text-primary-200 hover:text-white font-medium transition-colors
|
||||
{% endif %}" href="{{ url_for('main.dashboard') }}">Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-3">
|
||||
<a class="inline-block py-2 px-4 no-underline
|
||||
{% if request.path == url_for('data.manage_data') %}
|
||||
text-white
|
||||
{% else %}
|
||||
text-primary-200 hover:text-white font-medium transition-colors
|
||||
{% endif %}" href="{{ url_for('data.manage_data') }}">Data
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-3">
|
||||
<a class="flex items-center gap-2 text-primary-200 no-underline hover:text-white font-medium transition-colors py-2 px-4"
|
||||
href="{{ url_for('user.profile') }}">
|
||||
<img src="{{ url_for('user.profile_image', user_id=current_user.id) }}" alt="Profile Picture"
|
||||
class="w-8 h-8 rounded-full border-2 border-white object-cover group-hover:scale-105 transition">
|
||||
<span class=" text-sm font-medium group-hover:underline
|
||||
{% if request.path == url_for('user.profile') %}
|
||||
text-white
|
||||
{% else %}
|
||||
text-primary-200 hover:text-white font-medium transition-colors
|
||||
{% endif %}">Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-3">
|
||||
<a class="inline-block py-2 px-4 no-underline
|
||||
{% if request.path == url_for('auth.logout') %}
|
||||
text-white
|
||||
{% else %}
|
||||
text-primary-200 hover:text-white font-medium transition-colors
|
||||
{% endif %}" href="{{ url_for('auth.logout') }}">Logout
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="mr-3">
|
||||
<a class="inline-block py-2 px-4 no-underline
|
||||
{% if request.path == url_for('auth.login') %}
|
||||
text-white
|
||||
{% else %}
|
||||
text-primary-200 hover:text-white font-medium transition-colors
|
||||
{% endif %}" href="{{ url_for('auth.login') }}">Login
|
||||
</a>
|
||||
</li>
|
||||
<li class="mr-3">
|
||||
<a class="inline-block py-2 px-4 no-underline
|
||||
{% if request.path == url_for('auth.signup') %}
|
||||
text-white
|
||||
{% else %}
|
||||
text-primary-200 hover:text-white font-medium transition-colors
|
||||
{% endif %}" href="{{ url_for('auth.signup') }}">Signup
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
<div class="fixed top-24 right-4 z-50 space-y-4">
|
||||
{% for category, message in messages %}
|
||||
<div
|
||||
class="flash-message flex items-center justify-between p-4 rounded-xl shadow-xl text-white bg-{{ 'red' if category == 'danger' else 'primary' }}-500 min-w-[300px] transition-all duration-300">
|
||||
<span class="font-medium">{{ message }}</span>
|
||||
<button
|
||||
class="flash-close-btn text-2xl font-bold ml-4 hover:text-gray-200 transition-colors">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto mt-24">
|
||||
{% block content %}
|
||||
<!-- Content goes here -->
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-800 text-white py-4 mt-10">
|
||||
<div class="container mx-auto text-center">
|
||||
<p>© 2024 BP Tracker. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Use document for event delegation since body is replaced by diy-turbo
|
||||
document.addEventListener('click', async (e) => {
|
||||
// Flash messages close button
|
||||
const flashCloseBtn = e.target.closest('.flash-close-btn');
|
||||
if (flashCloseBtn) {
|
||||
const el = flashCloseBtn.closest('.flash-message');
|
||||
if (el) {
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => el.remove(), 300);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Micro-HTMX implementation
|
||||
const htmxTrigger = e.target.closest('[hx-get]');
|
||||
if (htmxTrigger) {
|
||||
if (htmxTrigger.tagName === 'FORM') return; // Let submit handler deal with forms
|
||||
e.preventDefault();
|
||||
const url = htmxTrigger.getAttribute('hx-get');
|
||||
const targetSelector = htmxTrigger.getAttribute('hx-target');
|
||||
if (!url || !targetSelector) return;
|
||||
|
||||
const targetEl = document.querySelector(targetSelector);
|
||||
if (!targetEl) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Fetch failed');
|
||||
targetEl.innerHTML = await response.text();
|
||||
} catch (err) {
|
||||
console.error('Micro-HTMX error:', err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Mobile Menu Toggle Button
|
||||
const menuBtn = e.target.closest('#mobile-menu-btn');
|
||||
if (menuBtn) {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
const nav = document.getElementById('mobile-nav');
|
||||
const iconMenu = document.getElementById('icon-menu');
|
||||
const iconClose = document.getElementById('icon-close');
|
||||
|
||||
if (menu) {
|
||||
const isHidden = menu.classList.contains('hidden');
|
||||
if (!isHidden) {
|
||||
menu.classList.add('hidden');
|
||||
nav.classList.replace('bg-primary-900', 'bg-primary-800');
|
||||
iconMenu.classList.remove('hidden');
|
||||
iconClose.classList.add('hidden');
|
||||
menuBtn.classList.remove('rotate-180', 'transform');
|
||||
} else {
|
||||
menu.classList.remove('hidden');
|
||||
nav.classList.replace('bg-primary-800', 'bg-primary-900');
|
||||
iconMenu.classList.add('hidden');
|
||||
iconClose.classList.remove('hidden');
|
||||
menuBtn.classList.add('rotate-180', 'transform');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle clicks outside of nav/menu to close mobile menu
|
||||
const nav = document.getElementById('mobile-nav');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
if (nav && menu && !nav.contains(e.target) && !menu.classList.contains('hidden')) {
|
||||
menu.classList.add('hidden');
|
||||
nav.classList.replace('bg-primary-900', 'bg-primary-800');
|
||||
const iconMenu = document.getElementById('icon-menu');
|
||||
const iconClose = document.getElementById('icon-close');
|
||||
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
||||
|
||||
if (iconMenu) iconMenu.classList.remove('hidden');
|
||||
if (iconClose) iconClose.classList.add('hidden');
|
||||
if (mobileMenuBtn) mobileMenuBtn.classList.remove('rotate-180', 'transform');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Escape key to close mobile menu
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
if (menu && !menu.classList.contains('hidden')) {
|
||||
const nav = document.getElementById('mobile-nav');
|
||||
menu.classList.add('hidden');
|
||||
if (nav) nav.classList.replace('bg-primary-900', 'bg-primary-800');
|
||||
|
||||
const iconMenu = document.getElementById('icon-menu');
|
||||
const iconClose = document.getElementById('icon-close');
|
||||
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
||||
|
||||
if (iconMenu) iconMenu.classList.remove('hidden');
|
||||
if (iconClose) iconClose.classList.add('hidden');
|
||||
if (mobileMenuBtn) mobileMenuBtn.classList.remove('rotate-180', 'transform');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-submit forms when inputs change
|
||||
document.addEventListener('change', (e) => {
|
||||
const htmxForm = e.target.closest('form[hx-get], form[hx-post]');
|
||||
if (htmxForm) {
|
||||
htmxForm.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle form submissions for Micro-HTMX
|
||||
document.addEventListener('submit', async (e) => {
|
||||
const htmxForm = e.target.closest('form[hx-get], form[hx-post]');
|
||||
if (htmxForm) {
|
||||
e.preventDefault();
|
||||
|
||||
const method = htmxForm.hasAttribute('hx-post') ? 'POST' : 'GET';
|
||||
let url = htmxForm.getAttribute('hx-get') || htmxForm.getAttribute('hx-post');
|
||||
const targetSelector = htmxForm.getAttribute('hx-target');
|
||||
|
||||
if (!url || !targetSelector) return;
|
||||
|
||||
const targetEl = document.querySelector(targetSelector);
|
||||
if (!targetEl) return;
|
||||
|
||||
try {
|
||||
let fetchOpts = { method };
|
||||
if (method === 'GET') {
|
||||
const formData = new FormData(htmxForm);
|
||||
const params = new URLSearchParams(formData).toString();
|
||||
if (params) {
|
||||
url += (url.includes('?') ? '&' : '?') + params;
|
||||
}
|
||||
} else {
|
||||
fetchOpts.body = new FormData(htmxForm);
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOpts);
|
||||
if (!response.ok) throw new Error('Fetch failed');
|
||||
targetEl.innerHTML = await response.text();
|
||||
} catch (err) {
|
||||
console.error('Micro-HTMX submit error:', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle auto-loading elements
|
||||
const handleAutoLoads = async () => {
|
||||
const elements = document.querySelectorAll('[hx-trigger="load"][hx-get]');
|
||||
for (const el of elements) {
|
||||
const url = el.getAttribute('hx-get');
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Auto-fetch failed');
|
||||
el.innerHTML = await response.text();
|
||||
el.removeAttribute('hx-trigger'); // ensure it only loads once per page view
|
||||
} catch (err) {
|
||||
console.error('Micro-HTMX autoload error:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', handleAutoLoads);
|
||||
document.addEventListener('diy-turbo:load', handleAutoLoads);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="{{ 'dark' if current_user.is_authenticated and current_user.profile and current_user.profile.dark_mode else 'light' }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>{% block title %}BP Tracker{% endblock %}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset_url('images/favicon.svg') }}">
|
||||
<link rel="stylesheet" href="{{ asset_url('css/tailwind.css') }}">
|
||||
<script defer src="{{ asset_url('js/diy-turbo.min.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<header class="site-header">
|
||||
<div class="shell header-inner">
|
||||
<a class="brand" href="{{ url_for('main.landing') }}" aria-label="BP Tracker home"><span class="brand-mark" aria-hidden="true"><svg viewBox="0 0 32 32" fill="none"><path d="M4 17h6l3-8 6 16 3-8h6" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>BP <span class="brand-light">Tracker</span></a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="main-nav" aria-label="Main navigation">
|
||||
{% for key, label in [('overview', 'Overview'), ('history', 'History'), ('trends', 'Trends')] %}
|
||||
<a href="{{ url_for('main.dashboard', view=key, **(selected.params if selected is defined and not calendar else {})) }}" {% if view is defined and view == key %}aria-current="page"{% endif %}>{{ label }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
<details class="account-menu">
|
||||
<summary aria-label="Account menu"><span class="avatar">{{ current_user.username[0]|upper }}</span><span class="account-label">Account</span><span aria-hidden="true">⌄</span></summary>
|
||||
<nav aria-label="Account"><a href="{{ url_for('data.manage_data') }}">Import & export</a><a href="{{ url_for('user.profile') }}">Settings</a><a href="{{ url_for('auth.logout') }}">Log out</a></nav>
|
||||
</details>
|
||||
{% else %}
|
||||
<nav class="actions" aria-label="Account"><a href="{{ url_for('auth.login') }}">Log in</a><a class="button primary" href="{{ url_for('auth.signup') }}">Get started</a></nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
<main id="main" class="shell main-content" tabindex="-1">
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}<div class="notifications" aria-label="Notifications">
|
||||
{% for category, message in messages %}
|
||||
<div class="notice {{ 'notice-error' if category == 'danger' else '' }}" role="{{ 'alert' if category == 'danger' else 'status' }}">
|
||||
<span><strong>{{ 'Please check' if category == 'danger' else 'Done' }}</strong> · {{ message }}</span>
|
||||
<button type="button" class="notice-close" aria-label="Dismiss notification" data-dismiss>×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer class="shell site-footer"><span>BP Tracker</span><span>A little clarity, every day.</span></footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,88 +1,4 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto mt-12 mb-20">
|
||||
<div class="bg-white p-10 rounded-3xl shadow-2xl border border-gray-100 relative overflow-hidden">
|
||||
<!-- Decorative background element -->
|
||||
<div class="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-primary-400 to-primary-600"></div>
|
||||
|
||||
<h1 class="text-3xl font-extrabold text-center mb-2 text-gray-900">Welcome Back</h1>
|
||||
<p class="text-center text-gray-500 mb-8">Please enter your details to sign in.</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.login') }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username Field -->
|
||||
<div class="mb-5">
|
||||
{{ form.username.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ form.username(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||
focus:bg-white") }}
|
||||
</div>
|
||||
{% for error in form.username.errors %}
|
||||
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="mb-5">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
{{ form.password.label(class="block text-sm font-semibold text-gray-700") }}
|
||||
<a href="#"
|
||||
class="text-sm text-primary-600 hover:text-primary-800 hover:underline font-medium">Forgot
|
||||
Password?</a>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ form.password(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||
focus:bg-white") }}
|
||||
</div>
|
||||
{% for error in form.password.errors %}
|
||||
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-8">
|
||||
{{ form.submit(class="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold py-3 rounded-xl
|
||||
shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-0.5 cursor-pointer")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<p class="text-center mt-6 text-gray-600">
|
||||
Don't have an account? <a href="{{ url_for('auth.signup') }}"
|
||||
class="text-primary-600 hover:text-primary-800 font-bold hover:underline">Sign up</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% from 'components.html' import field %}
|
||||
{% block title %}Log in · BP Tracker{% endblock %}
|
||||
{% block content %}<div class="form-page auth-page"><div class="page-heading"><div><p class="eyebrow">A LITTLE CLARITY, EVERY DAY</p><h1>Welcome back</h1><p class="muted">Log in to see your readings.</p></div></div><form method="post" class="card entry-form" novalidate>{{ form.hidden_tag() }}{{ field(form.username, autocomplete='username') }}{{ field(form.password, autocomplete='current-password') }}{% if form.csrf_token is defined and form.csrf_token.errors %}<p class="field-error" role="alert">Your form expired. Please try again.</p>{% endif %}<button class="button primary full-width" type="submit">Log in</button><p class="muted small">New here? <a href="{{ url_for('auth.signup') }}">Create an account</a></p></form></div>{% endblock %}
|
||||
|
||||
@@ -1,111 +1,4 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto mt-12 mb-20">
|
||||
<div class="bg-white p-10 rounded-3xl shadow-2xl border border-gray-100 relative overflow-hidden">
|
||||
<!-- Decorative background element -->
|
||||
<div class="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-primary-400 to-primary-600"></div>
|
||||
|
||||
<h1 class="text-3xl font-extrabold text-center mb-2 text-gray-900">Create Account</h1>
|
||||
<p class="text-center text-gray-500 mb-8">Join us to start tracking your health.</p>
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.signup') }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username Field -->
|
||||
<div class="mb-5">
|
||||
{{ form.username.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ form.username(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||
focus:bg-white") }}
|
||||
</div>
|
||||
{% for error in form.username.errors %}
|
||||
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="mb-5">
|
||||
{{ form.password.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ form.password(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||
focus:bg-white") }}
|
||||
</div>
|
||||
{% for error in form.password.errors %}
|
||||
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div class="mb-6">
|
||||
{{ form.confirm_password.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ form.confirm_password(class="w-full pl-11 p-3 border border-gray-300 rounded-xl
|
||||
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all
|
||||
shadow-sm bg-gray-50 focus:bg-white") }}
|
||||
</div>
|
||||
{% for error in form.confirm_password.errors %}
|
||||
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-8">
|
||||
{{ form.submit(class="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold py-3 rounded-xl
|
||||
shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-0.5 cursor-pointer")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<p class="text-center mt-6 text-gray-600">
|
||||
Already have an account? <a href="{{ url_for('auth.login') }}"
|
||||
class="text-primary-600 hover:text-primary-800 font-bold hover:underline">Log in</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% from 'components.html' import field %}
|
||||
{% block title %}Create account · BP Tracker{% endblock %}
|
||||
{% block content %}<div class="form-page auth-page"><div class="page-heading"><div><p class="eyebrow">START YOUR RECORD</p><h1>Create an account</h1><p class="muted">Keep your blood pressure readings together.</p></div></div><form method="post" class="card entry-form" novalidate>{{ form.hidden_tag() }}{{ field(form.username, autocomplete='username') }}{{ field(form.password, autocomplete='new-password') }}{{ field(form.confirm_password, autocomplete='new-password') }}{% if form.csrf_token is defined and form.csrf_token.errors %}<p class="field-error" role="alert">Your form expired. Please try again.</p>{% endif %}<button class="button primary full-width" type="submit">Create account</button><p class="muted small">Already tracking? <a href="{{ url_for('auth.login') }}">Log in</a></p></form></div>{% endblock %}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{% macro field(input, numeric=false, autocomplete='off') %}
|
||||
<div class="field">
|
||||
{{ input.label }}
|
||||
{% if numeric %}
|
||||
{{ input(inputmode='numeric', autocomplete=autocomplete, aria_invalid='true' if input.errors else 'false', aria_describedby=input.id ~ '-error' if input.errors else '') }}
|
||||
{% else %}
|
||||
{{ input(autocomplete=autocomplete, aria_invalid='true' if input.errors else 'false', aria_describedby=input.id ~ '-error' if input.errors else '') }}
|
||||
{% endif %}
|
||||
{% if input.errors %}<div class="field-error" id="{{ input.id }}-error">{% for error in input.errors %}<p>{{ error }}</p>{% endfor %}</div>{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro reading_rows(readings, sys_threshold, dia_threshold) %}
|
||||
{% if readings %}
|
||||
<div class="reading-table" role="table" aria-label="Blood pressure readings">
|
||||
<div class="reading-row table-heading" role="row"><span role="columnheader">Date & time</span><span role="columnheader">Blood pressure</span><span role="columnheader">Pulse</span><span role="columnheader">Personal threshold</span><span role="columnheader">Action</span></div>
|
||||
{% for reading in readings %}
|
||||
{% set above = reading.systolic >= sys_threshold or reading.diastolic >= dia_threshold %}
|
||||
<div class="reading-row" role="row">
|
||||
<div role="cell"><time datetime="{{ reading.local_timestamp.isoformat() }}">{{ reading.local_timestamp.strftime('%d %b %Y') }}<small>{{ reading.local_timestamp.strftime('%I:%M %p') }}</small></time></div>
|
||||
<div role="cell" class="reading-value"><span class="mobile-label">Blood pressure</span>{{ reading.systolic }}<span class="slash"> / </span>{{ reading.diastolic }}<small>mmHg</small></div>
|
||||
<div role="cell"><span class="mobile-label">Pulse</span><strong>{{ reading.heart_rate }}</strong> <small class="inline">bpm</small></div>
|
||||
<div role="cell"><span class="status {{ 'status-above' if above else '' }}">{{ 'At / above' if above else 'Below' }} threshold</span></div>
|
||||
<div role="cell"><a class="edit-link" aria-label="Edit reading from {{ reading.local_timestamp.strftime('%d %b %Y at %I:%M %p') }}" href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}">Edit <span aria-hidden="true">↗</span></a></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state"><span class="empty-symbol" aria-hidden="true">+</span><h3>No readings in this period</h3><p>Add a reading or choose another date range.</p><a class="button" href="{{ url_for('reading.add_reading') }}">Add reading</a></div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
+34
-114
@@ -1,114 +1,34 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-5xl mx-auto p-4 space-y-6">
|
||||
<!-- Header Section with "Add New Reading" Button -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-800">Dashboard</h1>
|
||||
<a rel="prefetch" href="{{ url_for('reading.add_reading') }}"
|
||||
class="bg-primary-600 text-white px-4 py-2 rounded shadow hover:bg-primary-700">
|
||||
+ Add New Reading
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Summary -->
|
||||
<div class="bg-gradient-to-r from-primary-500 to-primary-700 text-white p-6 rounded-xl shadow-md">
|
||||
<h3 class="text-lg font-bold">Weekly Summary</h3>
|
||||
<div class="flex justify-between mt-4">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Systolic Average</p>
|
||||
<p class="text-2xl">{{ systolic_avg }} <span class="text-base">mmHg</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Diastolic Average</p>
|
||||
<p class="text-2xl">{{ diastolic_avg }} <span class="text-base">mmHg</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Heart Rate Average</p>
|
||||
<p class="text-2xl">{{ heart_rate_avg }} <span class="text-base">bpm</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Badges -->
|
||||
<div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{% for badge in badges %}
|
||||
<div class="bg-green-100 text-green-800 px-4 py-2 rounded shadow text-sm font-medium">
|
||||
{{ badge }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b mb-4 overflow-x-auto" id="dashboard-tabs">
|
||||
<button hx-get="{{ url_for('main.dashboard_list') }}" hx-target="#dashboard-content"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap {{ 'border-primary-600 text-primary-600' if active_view == 'list' else 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">List
|
||||
View</button>
|
||||
<button hx-get="{{ url_for('main.dashboard_table') }}" hx-target="#dashboard-content"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap {{ 'border-primary-600 text-primary-600' if active_view == 'table' else 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">Table
|
||||
View</button>
|
||||
<button hx-get="{{ url_for('main.dashboard_weekly') }}" hx-target="#dashboard-content"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap {{ 'border-primary-600 text-primary-600' if active_view == 'weekly' else 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">Weekly
|
||||
View</button>
|
||||
<button hx-get="{{ url_for('main.dashboard_monthly') }}" hx-target="#dashboard-content"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap {{ 'border-primary-600 text-primary-600' if active_view == 'monthly' else 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">Monthly
|
||||
View</button>
|
||||
<button hx-get="{{ url_for('main.dashboard_graph') }}" hx-target="#dashboard-content"
|
||||
class="tab-btn px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap {{ 'border-primary-600 text-primary-600' if active_view == 'graph' else 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">Graph
|
||||
View</button>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Content Target Area for HTMX -->
|
||||
<div id="dashboard-content" hx-get="{{ url_for('main.dashboard_list') }}" hx-trigger="load">
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Use event delegation for dashboard interactions to survive DIY Turbo page replacements
|
||||
document.addEventListener('click', (e) => {
|
||||
// Filter Form Toggle
|
||||
const filterBtn = e.target.closest('#filter-btn');
|
||||
if (filterBtn) {
|
||||
const filterForm = document.getElementById('filter-form');
|
||||
const iconClosed = document.getElementById('filter-icon-closed');
|
||||
const iconOpen = document.getElementById('filter-icon-open');
|
||||
|
||||
if (filterForm) {
|
||||
const isHidden = filterForm.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
filterForm.classList.remove('hidden');
|
||||
if (iconClosed) iconClosed.classList.add('hidden');
|
||||
if (iconOpen) iconOpen.classList.remove('hidden');
|
||||
} else {
|
||||
filterForm.classList.add('hidden');
|
||||
if (iconClosed) iconClosed.classList.remove('hidden');
|
||||
if (iconOpen) iconOpen.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Tabs
|
||||
const tabBtn = e.target.closest('.tab-btn');
|
||||
if (tabBtn) {
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
tabBtns.forEach(b => {
|
||||
b.classList.remove('border-primary-600', 'text-primary-600');
|
||||
});
|
||||
tabBtn.classList.add('border-primary-600', 'text-primary-600');
|
||||
// The click will still bubble up and be caught by the Micro-HTMX logic in _layout.html
|
||||
return;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% from 'components.html' import reading_rows %}
|
||||
{% block title %}{{ view|title }} · BP Tracker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-heading"><div><p class="eyebrow">YOUR DAILY PICTURE</p><h1>{{ view|title }}</h1><p class="muted">{{ 'A clearer view of your blood pressure.' if view == 'overview' else 'Every reading, in one place.' if view == 'history' else 'See how your readings change over time.' }}</p></div><a class="button primary add-action" href="{{ url_for('reading.add_reading') }}"><span aria-hidden="true">+</span> Add reading</a></div>
|
||||
{% if not calendar %}{% include 'partials/range_filter.html' %}{% endif %}
|
||||
{% if view == 'overview' %}
|
||||
<section class="summary-grid" aria-label="Summary">
|
||||
<article class="card latest-card"><div class="card-label"><h2>Latest reading</h2><span class="dot" aria-hidden="true"></span></div>
|
||||
{% if latest %}<p class="big-number">{{ latest.systolic }}<span class="slash"> / </span>{{ latest.diastolic }} <span class="unit">mmHg</span></p><p class="latest-meta">{{ latest.heart_rate }} bpm <span>·</span> {{ latest.local_timestamp.strftime('%d %b, %I:%M %p') }}</p><p class="card-foot">Most recent, across all dates</p>
|
||||
{% else %}<p class="empty-title">Your first reading starts here</p><p class="muted">Record your blood pressure and pulse to get started.</p>{% endif %}</article>
|
||||
<article class="card"><h2>Period average</h2>{% if stats.count %}<p class="big-number">{{ stats.systolic }}<span class="slash"> / </span>{{ stats.diastolic }}</p><p class="muted">mmHg · {{ stats.pulse }} bpm average pulse</p>{% else %}<p class="empty-title">No readings yet</p><p class="muted">Averages appear when you add a reading.</p>{% endif %}<p class="card-foot">{{ selected.start.strftime('%d %b') }} – {{ selected.end.strftime('%d %b %Y') }}</p></article>
|
||||
<article class="card count-card"><h2>Readings recorded</h2><p class="big-number">{{ stats.count }}</p><p class="muted">In the selected period</p><p class="card-foot">Times shown in {{ timezone_name }}</p></article>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if view in ['overview', 'trends'] %}{% include 'partials/trend.html' %}{% endif %}
|
||||
{% if view == 'history' %}
|
||||
<div class="section-heading"><nav class="segmented" aria-label="History display"><a href="{{ url_for('main.dashboard', view='history', **selected.params) }}" {% if not calendar %}aria-current="page"{% endif %}>Readings</a><a href="{{ url_for('main.dashboard', view='history', mode='calendar') }}" {% if calendar %}aria-current="page"{% endif %}>Calendar</a></nav><span class="muted small">{{ stats.count }} readings · {{ timezone_name }}</span></div>
|
||||
{% endif %}
|
||||
{% if calendar %}
|
||||
<section class="card calendar-card">
|
||||
<div class="section-heading"><h2>{{ selected.start.strftime('%d %b') }} – {{ selected.end.strftime('%d %b %Y') }}</h2><div class="actions"><a class="button" aria-label="Previous {{ calendar.unit }}" href="{{ url_for('main.dashboard', view='history', mode='calendar', unit=calendar.unit, anchor=calendar.prev) }}">←</a><a class="button" aria-label="Next {{ calendar.unit }}" href="{{ url_for('main.dashboard', view='history', mode='calendar', unit=calendar.unit, anchor=calendar.next) }}">→</a></div></div>
|
||||
<form class="calendar-controls" action="{{ url_for('main.dashboard') }}" method="get"><input type="hidden" name="view" value="history"><input type="hidden" name="mode" value="calendar"><div class="field"><label for="unit">Display</label><select name="unit" id="unit"><option value="month" {% if calendar.unit == 'month' %}selected{% endif %}>Month</option><option value="week" {% if calendar.unit == 'week' %}selected{% endif %}>Week</option></select></div><div class="field"><label for="anchor">Containing date</label><input id="anchor" type="date" name="anchor" value="{{ calendar.anchor }}" required></div><button class="button" type="submit">Go</button></form>
|
||||
<p class="muted small">Daily averages in mmHg. Select a day to see its readings.</p>
|
||||
<div class="calendar-grid" style="--start-day:{{ selected.start.weekday() + 1 }}">{% for day in calendar.days %}<a class="calendar-day {{ 'has-readings' if day.data else '' }}" href="{{ url_for('main.dashboard', view='history', start_date=day.date.isoformat(), end_date=day.date.isoformat()) }}"><span class="muted small">{{ day.date.strftime('%a') }}</span><strong>{{ day.date.day }}</strong>{% if day.data %}<span>{{ day.data.systolic }} / {{ day.data.diastolic }}</span><small>{{ day.data.count }} reading{{ 's' if day.data.count != 1 else '' }}</small>{% else %}<small>No readings</small>{% endif %}</a>{% endfor %}</div>
|
||||
</section>
|
||||
{% elif view in ['overview', 'history'] %}
|
||||
<section class="card history-card"><div class="section-heading"><div><h2>{{ 'Recent readings' if view == 'overview' else 'Reading history' }}</h2><p class="muted small">Personal thresholds: {{ sys_threshold }}/{{ dia_threshold }} mmHg · <a href="{{ url_for('user.profile') }}">Adjust in Settings</a></p></div>{% if view == 'overview' %}<a class="text-link" href="{{ url_for('main.dashboard', view='history', **selected.params) }}">View all <span aria-hidden="true">→</span></a>{% endif %}</div>
|
||||
{{ reading_rows(readings, sys_threshold, dia_threshold) }}
|
||||
{% if pagination and pagination.pages > 1 %}<nav class="pagination" aria-label="History pages">{% if pagination.has_prev %}<a class="button" href="{{ url_for('main.dashboard', view='history', page=pagination.prev_num, **selected.params) }}">← Previous</a>{% endif %}<span>Page {{ pagination.page }} of {{ pagination.pages }}</span>{% if pagination.has_next %}<a class="button" href="{{ url_for('main.dashboard', view='history', page=pagination.next_num, **selected.params) }}">Next →</a>{% endif %}</nav>{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<div class="report-links"><span class="muted small">Keep a copy of this period</span><a href="{{ url_for('data.export_data', **selected.params) }}" download>Download CSV</a><a href="{{ url_for('main.report', **selected.params) }}">Printable summary ↗</a></div>
|
||||
{% endblock %}
|
||||
|
||||
+3
-39
@@ -1,39 +1,3 @@
|
||||
{% extends "_layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto p-4 relative">
|
||||
<!-- Cancel Button (Top-Left) -->
|
||||
<a href="{{ request.referrer if request.referrer else url_for('main.dashboard') }}"
|
||||
class="absolute top-5 left-4 flex items-center text-gray-600 hover:text-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold mb-4 text-center">Import/Export Data</h1>
|
||||
|
||||
<!-- Import Data Section -->
|
||||
<div class="bg-white p-6 rounded-lg shadow-md mb-6">
|
||||
<h2 class="text-lg font-semibold mb-4">Import Data</h2>
|
||||
<form method="POST" action="{{ url_for('data.manage_data') }}" enctype="multipart/form-data">
|
||||
<label for="file" class="block text-sm font-medium text-gray-700 mb-2">Upload CSV File</label>
|
||||
<input type="file" name="file" id="file"
|
||||
class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<button type="submit"
|
||||
class="mt-4 bg-blue-600 text-white px-6 py-2 rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
Import Data
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Export Data Section -->
|
||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-lg font-semibold mb-4">Export Data</h2>
|
||||
<a href="{{ url_for('data.export_data') }}"
|
||||
class="bg-green-600 text-white px-6 py-2 rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500">
|
||||
Download CSV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% block title %}Import & export · BP Tracker{% endblock %}
|
||||
{% block content %}<div class="page-heading"><div><p class="eyebrow">YOUR DATA</p><h1>Import & export</h1><p class="muted">Bring your readings together, or keep a copy.</p></div></div><div class="two-columns data-columns"><section class="card"><h2>Import readings</h2><p class="muted">Upload a CSV with Timestamp, Systolic, Diastolic and Heart Rate columns. Timestamps must be UTC, formatted YYYY-MM-DD HH:MM:SS.</p><form method="post" enctype="multipart/form-data" class="entry-form">{{ form.hidden_tag() }}<div class="field"><label for="file">CSV file</label><input type="file" id="file" name="file" accept=".csv,text/csv" required></div><p class="muted small">Imports add rows to your history, including duplicates. Maximum file size: 2 MB.</p><button class="button primary" type="submit">Import readings</button></form></section><section class="card"><h2>Export readings</h2><p class="muted">Download your full history as CSV. Exported timestamps use UTC for consistent re-imports.</p><a class="button" href="{{ url_for('data.export_data') }}" download>Download all readings</a><hr><h3>Need a specific period?</h3><p class="muted">Choose dates in History to download a filtered CSV or open a printable summary.</p><a class="text-link" href="{{ url_for('main.dashboard', view='history') }}">Go to History →</a></section></div>{% endblock %}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends '_layout.html' %}
|
||||
{% block title %}Choose a valid period · BP Tracker{% endblock %}
|
||||
{% block content %}<section class="card form-page"><h1>Check your dates</h1><p class="field-error" role="alert">{{ message }}</p><a class="button" href="{{ url_for('main.dashboard') }}">Back to overview</a></section>{% endblock %}
|
||||
@@ -1,84 +1,2 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 pb-20">
|
||||
<!-- Hero Section -->
|
||||
<section
|
||||
class="bg-gradient-to-br from-primary-600 to-primary-800 text-white rounded-b-3xl shadow-xl overflow-hidden relative">
|
||||
<div class="absolute inset-0 bg-white/5 pattern-dots pointer-events-none"></div>
|
||||
<div class="container mx-auto px-4 py-24 text-center relative z-10">
|
||||
<h1 class="text-5xl md:text-6xl font-extrabold mb-6 tracking-tight">
|
||||
Welcome to BP Tracker
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl mb-12 max-w-2xl mx-auto font-medium text-primary-50 leading-relaxed">
|
||||
Track your blood pressure and heart rate effortlessly. Take control of your health today!
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row justify-center space-y-4 sm:space-y-0 sm:space-x-6">
|
||||
<a href="{{ url_for('auth.signup') }}"
|
||||
class="px-8 py-4 bg-white text-primary-700 font-bold text-lg rounded-xl shadow-xl hover:shadow-2xl hover:bg-gray-50 transition-all duration-300 transform hover:-translate-y-1">
|
||||
Get Started
|
||||
</a>
|
||||
<a href="{{ url_for('auth.login') }}"
|
||||
class="px-8 py-4 bg-primary-700 text-white border border-primary-500 font-bold text-lg rounded-xl shadow-lg hover:shadow-xl hover:bg-primary-600 transition-all duration-300 transform hover:-translate-y-1">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="container mx-auto px-4 pt-24">
|
||||
<div class="text-center mb-16">
|
||||
<span class="text-primary-600 font-bold tracking-wider uppercase text-sm mb-2 block">Why Track With
|
||||
Us</span>
|
||||
<h2 class="text-4xl font-extrabold text-gray-900 tracking-tight">Why Choose BP Tracker?</h2>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-3 gap-10">
|
||||
<div
|
||||
class="text-center bg-white p-10 rounded-3xl shadow-lg border border-gray-100 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
|
||||
<div
|
||||
class="w-20 h-20 mx-auto bg-primary-50 rounded-2xl flex items-center justify-center mb-6 shadow-inner text-primary-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8c2.21 0 4 1.79 4 4s-1.79 4-4 4-4-1.79-4-4 1.79-4 4-4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-3">Accurate Tracking</h3>
|
||||
<p class="text-gray-600 leading-relaxed">Keep a detailed log of your blood pressure and heart rate over
|
||||
time to share with your doctor.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-center bg-white p-10 rounded-3xl shadow-lg border border-gray-100 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
|
||||
<div
|
||||
class="w-20 h-20 mx-auto bg-primary-50 rounded-2xl flex items-center justify-center mb-6 shadow-inner text-primary-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9.75 16.5L15 12m0 0l-5.25-4.5m5.25 4.5H3"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-3">Insightful Graphs</h3>
|
||||
<p class="text-gray-600 leading-relaxed">Visualize your progress and identify health trends with our
|
||||
intuitive, easy-to-read charts.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-center bg-white p-10 rounded-3xl shadow-lg border border-gray-100 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
|
||||
<div
|
||||
class="w-20 h-20 mx-auto bg-primary-50 rounded-2xl flex items-center justify-center mb-6 shadow-inner text-primary-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 20l-5.5-5.5M9 20V9m0 11h11"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-3">Secure and Private</h3>
|
||||
<p class="text-gray-600 leading-relaxed">Your medical data is protected with state-of-the-art security
|
||||
measures. Your privacy is paramount.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% block content %}<section class="landing-hero"><p class="eyebrow">A LITTLE CLARITY, EVERY DAY</p><h1>Your readings.<br>A clearer picture.</h1><p>Keep a simple record of your blood pressure and pulse.<br>See the trends. Bring your history to your next appointment.</p><div class="actions"><a class="button primary" href="{{ url_for('auth.signup') }}">Start tracking →</a><a class="button" href="{{ url_for('auth.login') }}">Log in</a></div></section><section class="summary-grid"><article class="card"><p class="eyebrow">01 / RECORD</p><h2>Quick to add</h2><p class="muted">Blood pressure, pulse and time. The essentials in one simple form.</p></article><article class="card"><p class="eyebrow">02 / UNDERSTAND</p><h2>See your patterns</h2><p class="muted">Explore your history with period averages and clear trends.</p></article><article class="card"><p class="eyebrow">03 / KEEP</p><h2>Take it with you</h2><p class="muted">Download your readings or print a summary for your records.</p></article></section>{% endblock %}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
<style>
|
||||
.svg-tooltip-group .svg-tooltip-content {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.svg-tooltip-group:hover .svg-tooltip-content {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
<div class="space-y-8">
|
||||
<!-- Graph Date Filter -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 mb-2">
|
||||
<form hx-get="{{ url_for('main.dashboard_graph') }}" hx-target="#dashboard-content"
|
||||
class="flex flex-col sm:flex-row gap-4">
|
||||
<!-- Start Date -->
|
||||
<div class="flex-1">
|
||||
<label for="start_date" class="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||
<input type="date" name="start_date" id="start_date" value="{{ start_date or '' }}"
|
||||
class="w-full p-2.5 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700">
|
||||
</div>
|
||||
|
||||
<!-- End Date -->
|
||||
<div class="flex-1">
|
||||
<label for="end_date" class="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||
<input type="date" name="end_date" id="end_date" value="{{ end_date or '' }}"
|
||||
class="w-full p-2.5 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Blood Pressure Graph Card -->
|
||||
<div class="bg-white rounded-2xl shadow-lg border border-gray-100 p-6 transition-all hover:shadow-xl">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-800">Blood Pressure Trends</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">Systolic vs Diastolic over time (mmHg)</p>
|
||||
</div>
|
||||
<div class="flex space-x-6 text-sm mt-4 md:mt-0 bg-gray-50 px-4 py-2 rounded-full">
|
||||
<div class="flex items-center"><span
|
||||
class="w-3 h-3 rounded-full bg-blue-500 mr-2 shadow-sm"></span><span
|
||||
class="font-medium text-gray-700">Systolic</span></div>
|
||||
<div class="flex items-center"><span
|
||||
class="w-3 h-3 rounded-full bg-pink-500 mr-2 shadow-sm"></span><span
|
||||
class="font-medium text-gray-700">Diastolic</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-auto pb-4">
|
||||
<svg viewBox="0 0 800 350" class="w-full min-w-[600px] h-auto drop-shadow-sm font-sans"
|
||||
style="max-height: 350px;">
|
||||
<defs>
|
||||
<linearGradient id="sysGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="diaGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#ec4899" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#ec4899" stop-opacity="0.0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{% if timestamps %}
|
||||
{% set n = timestamps|length %}
|
||||
{% set spacing = 700 / (n - 1) if n > 1 else 0 %}
|
||||
|
||||
<!-- Horizontal Grid Lines & Y-Axis Labels -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
{% set y = 280 - (value / 200 * 250) %}
|
||||
<line x1="50" y1="{{ y }}" x2="750" y2="{{ y }}" stroke="#e5e7eb" stroke-width="1"
|
||||
stroke-dasharray="4 4" />
|
||||
<text x="40" y="{{ y + 4 }}" font-size="12" fill="#9ca3af" text-anchor="end" font-weight="500">{{ value
|
||||
}}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-Axis Base Line -->
|
||||
<line x1="50" y1="280" x2="750" y2="280" stroke="#d1d5db" stroke-width="1" />
|
||||
|
||||
<!-- X-Axis Labels -->
|
||||
{% set ns = namespace(last_x=-100) %}
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
<!-- Optimization: only draw label if it has enough horizontal space from the last drawn label -->
|
||||
{% set is_last = (i == n - 1) %}
|
||||
{% if x - ns.last_x > 45 or is_last %}
|
||||
<!-- If it's the last label but too close to the previous, maybe we draw it anyway but it will overlap. We prevent overlap by aggressively spacing. -->
|
||||
{% if not is_last or x - ns.last_x > 40 or ns.last_x == -100 %}
|
||||
<text x="{{ x }}" y="310" font-size="11" fill="#6b7280" text-anchor="middle" font-weight="500"
|
||||
transform="rotate(-25 {{ x }} 310)">{{
|
||||
timestamps[i] }}</text>
|
||||
{% set ns.last_x = x %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Systolic Filled Area -->
|
||||
<polygon fill="url(#sysGrad)" points="{% if n>1 %}50{% else %}400{% endif %},280
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (systolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}
|
||||
{% if n>1 %}{{ 50 + (n - 1) * spacing }}{% else %}400{% endif %},280" />
|
||||
|
||||
<!-- Systolic Line -->
|
||||
<polyline fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
|
||||
points="{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (systolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}" />
|
||||
|
||||
<!-- Systolic Points -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (systolic[i] / 200 * 250) %}
|
||||
<circle cx="{{ x }}" cy="{{ y }}" r="5" fill="#ffffff" stroke="#3b82f6" stroke-width="2.5"></circle>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Diastolic Filled Area -->
|
||||
<polygon fill="url(#diaGrad)" points="{% if n>1 %}50{% else %}400{% endif %},280
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (diastolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}
|
||||
{% if n>1 %}{{ 50 + (n - 1) * spacing }}{% else %}400{% endif %},280" />
|
||||
|
||||
<!-- Diastolic Line -->
|
||||
<polyline fill="none" stroke="#ec4899" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
|
||||
points="{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (diastolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}" />
|
||||
|
||||
<!-- Diastolic Points -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (diastolic[i] / 200 * 250) %}
|
||||
<circle cx="{{ x }}" cy="{{ y }}" r="5" fill="#ffffff" stroke="#ec4899" stroke-width="2.5"></circle>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Tooltips (Rendered last to stay on top) -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
|
||||
{% set ySys = 280 - (systolic[i] / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: pointer;">
|
||||
<circle cx="{{ x }}" cy="{{ ySys }}" r="15" fill="transparent" />
|
||||
<circle cx="{{ x }}" cy="{{ ySys }}" r="7" fill="#ffffff" stroke="#3b82f6" stroke-width="2.5"
|
||||
class="svg-tooltip-content" />
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="{{ x - 64 }}" y="{{ ySys - 54 }}" width="128" height="40" fill="#1f2937" rx="6" />
|
||||
<polygon points="{{ x - 6 }},{{ ySys - 14 }} {{ x + 6 }},{{ ySys - 14 }} {{ x }},{{ ySys - 6 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="{{ x }}" y="{{ ySys - 36 }}" font-size="11" fill="#f3f4f6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">{{ timestamps[i] }}</text>
|
||||
<text x="{{ x }}" y="{{ ySys - 22 }}" font-size="11" fill="#93c5fd" text-anchor="middle"
|
||||
font-weight="500" class="font-sans">Systolic: {{ systolic[i] }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% set yDia = 280 - (diastolic[i] / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: pointer;">
|
||||
<circle cx="{{ x }}" cy="{{ yDia }}" r="15" fill="transparent" />
|
||||
<circle cx="{{ x }}" cy="{{ yDia }}" r="7" fill="#ffffff" stroke="#ec4899" stroke-width="2.5"
|
||||
class="svg-tooltip-content" />
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="{{ x - 64 }}" y="{{ yDia - 54 }}" width="128" height="40" fill="#1f2937" rx="6" />
|
||||
<polygon points="{{ x - 6 }},{{ yDia - 14 }} {{ x + 6 }},{{ yDia - 14 }} {{ x }},{{ yDia - 6 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="{{ x }}" y="{{ yDia - 36 }}" font-size="11" fill="#f3f4f6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">{{ timestamps[i] }}</text>
|
||||
<text x="{{ x }}" y="{{ yDia - 22 }}" font-size="11" fill="#f472b6" text-anchor="middle"
|
||||
font-weight="500" class="font-sans">Diastolic: {{ diastolic[i] }}</text>
|
||||
</g>
|
||||
</g>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Average Lines -->
|
||||
{% set ySysAvg = 280 - (sys_avg / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: default;">
|
||||
<line x1="50" y1="{{ ySysAvg }}" x2="750" y2="{{ ySysAvg }}" stroke="#3b82f6" stroke-width="2"
|
||||
stroke-dasharray="8 6" opacity="0.8" />
|
||||
<line x1="50" y1="{{ ySysAvg }}" x2="750" y2="{{ ySysAvg }}" stroke="transparent"
|
||||
stroke-width="15" /> <!-- Invisible hover anchor -->
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="336" y="{{ ySysAvg - 34 }}" width="128" height="26" fill="#1f2937" rx="6" />
|
||||
<polygon points="394,{{ ySysAvg - 8 }} 406,{{ ySysAvg - 8 }} 400,{{ ySysAvg - 2 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="400" y="{{ ySysAvg - 17 }}" font-size="11" fill="#93c5fd" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">Avg Systolic: {{ sys_avg }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% set yDiaAvg = 280 - (dia_avg / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: default;">
|
||||
<line x1="50" y1="{{ yDiaAvg }}" x2="750" y2="{{ yDiaAvg }}" stroke="#ec4899" stroke-width="2"
|
||||
stroke-dasharray="8 6" opacity="0.8" />
|
||||
<line x1="50" y1="{{ yDiaAvg }}" x2="750" y2="{{ yDiaAvg }}" stroke="transparent"
|
||||
stroke-width="15" /> <!-- Invisible hover anchor -->
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="336" y="{{ yDiaAvg - 34 }}" width="128" height="26" fill="#1f2937" rx="6" />
|
||||
<polygon points="394,{{ yDiaAvg - 8 }} 406,{{ yDiaAvg - 8 }} 400,{{ yDiaAvg - 2 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="400" y="{{ yDiaAvg - 17 }}" font-size="11" fill="#f472b6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">Avg Diastolic: {{ dia_avg }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% else %}
|
||||
<rect x="50" y="30" width="700" height="250" fill="#f9fafb" rx="10" />
|
||||
<text x="400" y="155" font-size="16" fill="#9ca3af" text-anchor="middle" font-weight="500">No data
|
||||
available for the selected period.</text>
|
||||
{% endif %}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Graph Card -->
|
||||
<div class="bg-white rounded-2xl shadow-lg border border-gray-100 p-6 transition-all hover:shadow-xl">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-800">Heart Rate</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">Beats per minute over time (bpm)</p>
|
||||
</div>
|
||||
<div class="flex space-x-6 text-sm mt-4 md:mt-0 bg-gray-50 px-4 py-2 rounded-full">
|
||||
<div class="flex items-center"><span
|
||||
class="w-3 h-3 rounded-full bg-emerald-500 mr-2 shadow-sm"></span><span
|
||||
class="font-medium text-gray-700">Heart Rate</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-auto pb-4">
|
||||
<svg viewBox="0 0 800 350" class="w-full min-w-[600px] h-auto drop-shadow-sm font-sans"
|
||||
style="max-height: 350px;">
|
||||
<defs>
|
||||
<linearGradient id="hrGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#10b981" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#10b981" stop-opacity="0.0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{% if timestamps %}
|
||||
{% set n = timestamps|length %}
|
||||
{% set spacing = 700 / (n - 1) if n > 1 else 0 %}
|
||||
|
||||
<!-- Horizontal Grid Lines & Y-Axis Labels -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
{% set y = 280 - (value / 200 * 250) %}
|
||||
<line x1="50" y1="{{ y }}" x2="750" y2="{{ y }}" stroke="#e5e7eb" stroke-width="1"
|
||||
stroke-dasharray="4 4" />
|
||||
<text x="40" y="{{ y + 4 }}" font-size="12" fill="#9ca3af" text-anchor="end" font-weight="500">{{ value
|
||||
}}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-Axis Base Line -->
|
||||
<line x1="50" y1="280" x2="750" y2="280" stroke="#d1d5db" stroke-width="1" />
|
||||
|
||||
<!-- X-Axis Labels -->
|
||||
{% set ns = namespace(last_x=-100) %}
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set is_last = (i == n - 1) %}
|
||||
{% if x - ns.last_x > 45 or is_last %}
|
||||
{% if not is_last or x - ns.last_x > 40 or ns.last_x == -100 %}
|
||||
<text x="{{ x }}" y="310" font-size="11" fill="#6b7280" text-anchor="middle" font-weight="500"
|
||||
transform="rotate(-25 {{ x }} 310)">{{
|
||||
timestamps[i] }}</text>
|
||||
{% set ns.last_x = x %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Heart Rate Filled Area -->
|
||||
<polygon fill="url(#hrGrad)" points="{% if n>1 %}50{% else %}400{% endif %},280
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}
|
||||
{% if n>1 %}{{ 50 + (n - 1) * spacing }}{% else %}400{% endif %},280" />
|
||||
|
||||
<!-- Heart Rate Line -->
|
||||
<polyline fill="none" stroke="#10b981" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
|
||||
points="{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}" />
|
||||
|
||||
<!-- Heart Rate Points -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
<circle cx="{{ x }}" cy="{{ y }}" r="5" fill="#ffffff" stroke="#10b981" stroke-width="2.5"></circle>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Tooltips (Rendered last to stay on top) -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set yHR = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: pointer;">
|
||||
<circle cx="{{ x }}" cy="{{ yHR }}" r="15" fill="transparent" />
|
||||
<circle cx="{{ x }}" cy="{{ yHR }}" r="7" fill="#ffffff" stroke="#10b981" stroke-width="2.5"
|
||||
class="svg-tooltip-content" />
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="{{ x - 64 }}" y="{{ yHR - 54 }}" width="128" height="40" fill="#1f2937" rx="6" />
|
||||
<polygon points="{{ x - 6 }},{{ yHR - 14 }} {{ x + 6 }},{{ yHR - 14 }} {{ x }},{{ yHR - 6 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="{{ x }}" y="{{ yHR - 36 }}" font-size="11" fill="#f3f4f6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">{{ timestamps[i] }}</text>
|
||||
<text x="{{ x }}" y="{{ yHR - 22 }}" font-size="11" fill="#34d399" text-anchor="middle"
|
||||
font-weight="500" class="font-sans">Heart Rate: {{ heart_rate[i] }}</text>
|
||||
</g>
|
||||
</g>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Average Line -->
|
||||
{% set yHrAvg = 280 - (hr_avg / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: default;">
|
||||
<line x1="50" y1="{{ yHrAvg }}" x2="750" y2="{{ yHrAvg }}" stroke="#10b981" stroke-width="2"
|
||||
stroke-dasharray="8 6" opacity="0.8" />
|
||||
<line x1="50" y1="{{ yHrAvg }}" x2="750" y2="{{ yHrAvg }}" stroke="transparent" stroke-width="15" />
|
||||
<!-- Invisible hover anchor -->
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="336" y="{{ yHrAvg - 34 }}" width="128" height="26" fill="#1f2937" rx="6" />
|
||||
<polygon points="394,{{ yHrAvg - 8 }} 406,{{ yHrAvg - 8 }} 400,{{ yHrAvg - 2 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="400" y="{{ yHrAvg - 17 }}" font-size="11" fill="#34d399" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">Avg Heart Rate: {{ hr_avg }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% else %}
|
||||
<rect x="50" y="30" width="700" height="250" fill="#f9fafb" rx="10" />
|
||||
<text x="400" y="155" font-size="16" fill="#9ca3af" text-anchor="middle" font-weight="500">No data
|
||||
available for the selected period.</text>
|
||||
{% endif %}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,79 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<defs>
|
||||
<path id="icon-clock" stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 8v4l3 3m9-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
||||
<path id="icon-chevron" stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</defs>
|
||||
</svg>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{% for reading in readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="bg-white shadow-sm hover:shadow-md rounded-xl p-3 flex justify-between items-center border border-gray-100 transition-all">
|
||||
|
||||
<!-- Left side: Timestamp & BP -->
|
||||
<div>
|
||||
<div class="flex items-center text-gray-400 text-xs mb-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span title="{{ reading.local_timestamp.strftime('%d %b %Y, %I:%M %p') }}">
|
||||
{{ reading.relative_timestamp }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<span class="text-xl font-bold text-gray-800">{{ reading.systolic }}<span
|
||||
class="text-gray-400 font-normal mx-0.5">/</span>{{ reading.diastolic }}</span>
|
||||
<span class="text-xs text-gray-500 ml-1">mmHg</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: Heart Rate & Icon -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<span class="text-xs text-gray-500 block mb-0.5">HR</span>
|
||||
<div class="flex items-baseline justify-end">
|
||||
<span class="text-lg font-bold text-gray-700">{{ reading.heart_rate }}</span>
|
||||
<span class="text-xs text-gray-400 ml-1">bpm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="h-4 w-4">
|
||||
<use href="#icon-chevron"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="col-span-full text-center text-sm text-gray-500">
|
||||
No readings found.
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="flex justify-center items-center gap-2 mt-6">
|
||||
{% if pagination.has_prev %}
|
||||
<button hx-get="{{ url_for('main.dashboard_list', page=pagination.prev_num) }}" hx-target="#dashboard-content"
|
||||
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">« Prev</button>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
<button hx-get="{{ url_for('main.dashboard_list', page=page_num) }}" hx-target="#dashboard-content"
|
||||
class="px-3 py-1 rounded text-sm {% if page_num == pagination.page %}bg-primary-600 text-white{% else %}bg-gray-200 hover:bg-gray-300{% endif %}">
|
||||
{{ page_num }}
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-gray-400">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<button hx-get="{{ url_for('main.dashboard_list', page=pagination.next_num) }}" hx-target="#dashboard-content"
|
||||
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">Next »</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
@@ -1,88 +0,0 @@
|
||||
<div class="flex flex-col px-2 py-2 -mb-px">
|
||||
<!-- Monthly Navigation -->
|
||||
<div class="flex justify-between items-center mb-4 px-2 mt-2">
|
||||
<button hx-get="{{ url_for('main.dashboard_monthly', month_offset=month_offset - 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Previous Month
|
||||
</button>
|
||||
|
||||
<h2 class="text-xl font-bold text-gray-800">{{ target_month_date.strftime('%B %Y') }}</h2>
|
||||
|
||||
<button hx-get="{{ url_for('main.dashboard_monthly', month_offset=month_offset + 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors {% if month_offset >= 0 %}opacity-50 pointer-events-none{% endif %}">
|
||||
Next Month
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 pl-2 pr-2">
|
||||
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Sunday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sun</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Monday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Mon</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Tuesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Tue</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Wednesday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Wed</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Thursday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Thu</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Friday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Fri</span>
|
||||
</div>
|
||||
<div class="p-2 h-10 text-center font-bold">
|
||||
<span class="xl:block lg:block md:block sm:block hidden">Saturday</span>
|
||||
<span class="xl:hidden lg:hidden md:hidden sm:hidden block">Sat</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 overflow-hidden flex-1 pl-2 pr-2 w-full">
|
||||
|
||||
{% for day in month %}
|
||||
<div
|
||||
class="{% if day.is_today %}border-2 border-primary-400 bg-primary-50{% else %}border bg-white{% endif %} flex flex-col min-h-[120px] p-1.5 transition-colors {% if not day.is_in_current_month %}bg-gray-50 opacity-50{% endif %}">
|
||||
<div class="text-right w-full mb-1">
|
||||
<span
|
||||
class="text-xs font-semibold {% if day.is_today %}text-primary-600{% else %}text-gray-500{% endif %}">{{
|
||||
day.day }}</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 flex-grow">
|
||||
{% for reading in day.readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="flex flex-col xl:flex-row justify-between items-center px-1 py-1 bg-white border border-gray-100 rounded shadow-sm hover:border-primary-300 hover:bg-primary-50 transition-colors">
|
||||
<span class="text-[10px] sm:text-xs font-bold text-gray-800 leading-none mb-0.5 xl:mb-0">{{
|
||||
reading.systolic }}/{{ reading.diastolic }}</span>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<span
|
||||
class="text-[9px] sm:text-[10px] font-medium bg-red-50 text-red-600 px-1 rounded leading-none py-0.5"
|
||||
title="Heart Rate">{{ reading.heart_rate }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,172 +0,0 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Table Date Filter -->
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 mb-2">
|
||||
<form hx-get="{{ url_for('main.dashboard_table') }}" hx-target="#dashboard-content"
|
||||
class="flex flex-col sm:flex-row gap-4">
|
||||
<!-- Start Date -->
|
||||
<div class="flex-1">
|
||||
<label for="start_date" class="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
||||
<input type="date" name="start_date" id="start_date" value="{{ start_date or '' }}"
|
||||
class="w-full p-2.5 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700">
|
||||
</div>
|
||||
|
||||
<!-- End Date -->
|
||||
<div class="flex-1">
|
||||
<label for="end_date" class="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
||||
<input type="date" name="end_date" id="end_date" value="{{ end_date or '' }}"
|
||||
class="w-full p-2.5 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Data Container -->
|
||||
<div class="bg-transparent md:bg-white md:rounded-2xl md:shadow-sm md:border md:border-gray-100 overflow-hidden">
|
||||
|
||||
<!-- Mobile Card View (Hidden on medium screens and up) -->
|
||||
<div class="md:hidden space-y-4">
|
||||
{% for reading in readings %}
|
||||
<div class="bg-white p-4 rounded-xl shadow-sm border border-gray-100 flex flex-col gap-3">
|
||||
<div class="flex justify-between items-center border-b border-gray-50 pb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-gray-400" 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-sm font-bold text-gray-800">{{ reading.local_timestamp.strftime('%Y-%m-%d')
|
||||
}}</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-500">{{ reading.local_timestamp.strftime('%I:%M %p')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Blood Pressure</span>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span
|
||||
class="font-bold text-lg {{ 'text-red-500' if reading.systolic >= 130 else 'text-gray-900' }}">{{
|
||||
reading.systolic }}</span>
|
||||
<span class="font-medium text-gray-400">/</span>
|
||||
<span
|
||||
class="font-bold text-lg {{ 'text-red-500' if reading.diastolic >= 80 else 'text-gray-900' }}">{{
|
||||
reading.diastolic }}</span>
|
||||
<span class="text-xs text-gray-500 font-medium ml-1">mmHg</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Heart Rate</span>
|
||||
<div class="flex items-baseline">
|
||||
<span class="font-bold text-lg text-gray-900">{{ reading.heart_rate }}</span>
|
||||
<span class="text-xs text-gray-500 font-medium ml-1">bpm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 mt-1 border-t border-gray-50">
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="inline-flex items-center gap-1 text-primary-600 hover:text-primary-800 text-sm font-bold transition-colors">
|
||||
Edit
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div
|
||||
class="bg-white p-8 text-center rounded-xl shadow-sm border border-gray-100 flex flex-col items-center gap-3">
|
||||
<svg class="w-12 h-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-500">No readings found for this date range.</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Desktop Table View (Hidden on small screens) -->
|
||||
<div class="hidden md:block overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Date</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Time</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Systolic</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Diastolic</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Heart Rate</th>
|
||||
<th scope="col"
|
||||
class="px-6 py-3 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for reading in readings %}
|
||||
<tr class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{
|
||||
reading.local_timestamp.strftime('%Y-%m-%d') }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{
|
||||
reading.local_timestamp.strftime('%I:%M %p') }}</td>
|
||||
<td
|
||||
class="px-6 py-4 whitespace-nowrap text-sm font-semibold {{ 'text-red-500' if reading.systolic >= 130 else 'text-gray-900' }}">
|
||||
{{ reading.systolic }}</td>
|
||||
<td
|
||||
class="px-6 py-4 whitespace-nowrap text-sm font-semibold {{ 'text-red-500' if reading.diastolic >= 80 else 'text-gray-900' }}">
|
||||
{{ reading.diastolic }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ reading.heart_rate }} bpm</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="text-primary-600 hover:text-primary-900">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center text-sm text-gray-500">
|
||||
No readings found for this date range.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="flex justify-center items-center gap-2 mt-6">
|
||||
{% if pagination.has_prev %}
|
||||
<button
|
||||
hx-get="{{ url_for('main.dashboard_table', page=pagination.prev_num, start_date=start_date, end_date=end_date) }}"
|
||||
hx-target="#dashboard-content" class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">«
|
||||
Prev</button>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
<button hx-get="{{ url_for('main.dashboard_table', page=page_num, start_date=start_date, end_date=end_date) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="px-3 py-1 rounded text-sm {% if page_num == pagination.page %}bg-primary-600 text-white{% else %}bg-gray-200 hover:bg-gray-300{% endif %}">
|
||||
{{ page_num }}
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-gray-400">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<button
|
||||
hx-get="{{ url_for('main.dashboard_table', page=pagination.next_num, start_date=start_date, end_date=end_date) }}"
|
||||
hx-target="#dashboard-content" class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">Next
|
||||
»</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,56 +0,0 @@
|
||||
<div>
|
||||
<!-- Weekly Navigation -->
|
||||
<div class="flex justify-between items-center mb-4 px-2">
|
||||
<button hx-get="{{ url_for('main.dashboard_weekly', week_offset=week_offset - 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Previous Week
|
||||
</button>
|
||||
|
||||
<span class="text-gray-600 font-semibold {% if week_offset == 0 %}text-primary-600{% endif %}">
|
||||
{% if week_offset == 0 %}This Week{% elif week_offset == -1 %}Last Week{% elif week_offset == 1 %}Next
|
||||
Week{% else %}{{ week_offset|abs }} weeks {% if week_offset < 0 %}ago{% else %}from now{% endif %}{% endif
|
||||
%} </span>
|
||||
|
||||
<button hx-get="{{ url_for('main.dashboard_weekly', week_offset=week_offset + 1) }}"
|
||||
hx-target="#dashboard-content"
|
||||
class="flex items-center text-primary-600 hover:text-primary-800 font-medium transition-colors {% if week_offset >= 0 %}opacity-50 pointer-events-none{% endif %}">
|
||||
Next Week
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 ml-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 text-center">
|
||||
{% for day in week %}
|
||||
<div class="border p-2 bg-gray-50 flex flex-col min-h-[140px]">
|
||||
<div class="text-sm font-bold text-gray-500 mb-2">{{ day.date }}</div>
|
||||
{% if day.readings %}
|
||||
<div class="space-y-1.5 flex-grow">
|
||||
{% for reading in day.readings %}
|
||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||
class="flex flex-col 2xl:flex-row justify-between items-center px-1.5 py-1 bg-white border border-gray-200 rounded shadow-sm hover:border-primary-400 hover:bg-primary-50 transition-colors">
|
||||
<span class="text-xs font-bold text-gray-800">{{ reading.systolic }}/{{ reading.diastolic
|
||||
}}</span>
|
||||
<div class="flex items-center gap-1 2xl:mt-0 mt-0.5">
|
||||
<span class="text-[10px] font-medium bg-red-50 text-red-600 px-1 rounded" title="Heart Rate">{{
|
||||
reading.heart_rate }}</span>
|
||||
<span class="text-[10px] text-gray-400 font-medium whitespace-nowrap">{{
|
||||
reading.local_timestamp.strftime('%H:%M') }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="flex-grow"></div> <!-- Spacer -->
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="range-bar">
|
||||
<nav class="segmented" aria-label="Quick date ranges">{% for days in [7, 30, 90] %}<a href="{{ url_for('main.dashboard', view=view, days=days) }}" {% if active_days == days %}aria-current="page"{% endif %}>{{ days }} days</a>{% endfor %}</nav>
|
||||
<form method="get" action="{{ url_for('main.dashboard') }}" class="date-form"><input type="hidden" name="view" value="{{ view }}"><div class="field"><label for="start_date">From</label><input id="start_date" name="start_date" type="date" value="{{ selected.start }}" required></div><div class="field"><label for="end_date">To</label><input id="end_date" name="end_date" type="date" value="{{ selected.end }}" required></div><button type="submit" class="button">Apply</button></form>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<section class="card trend-card">
|
||||
<div class="section-heading"><div><p class="eyebrow">THE BIGGER PICTURE</p><h2>Blood pressure trend</h2></div><span class="chart-unit">mmHg</span></div>
|
||||
<div class="chart-legend"><span><i class="legend-line systolic"></i>Systolic</span><span><i class="legend-line diastolic"></i>Diastolic</span>{% if stats.count %}<span class="muted small">Average {{ stats.systolic }} / {{ stats.diastolic }} · {{ stats.count }} readings</span>{% endif %}</div>
|
||||
{% if chart.points %}
|
||||
<svg class="trend-chart" viewBox="0 0 800 280" role="img" aria-labelledby="trend-title trend-desc"><title id="trend-title">Blood pressure over the selected period</title><desc id="trend-desc">{{ chart.bucket_days }}-day averages. Systolic average {{ stats.systolic }}, diastolic average {{ stats.diastolic }} mmHg. Exact averages and counts are available {{ 'in the expandable chart data below' if view == 'trends' else 'on the Trends page' }}.</desc>
|
||||
{% for tick in [50, 100, 150, 200, 250] %}{% set y = 244 - tick / 260 * 208 %}<line class="grid-line" x1="48" x2="752" y1="{{ y }}" y2="{{ y }}"/><text x="34" y="{{ y + 4 }}" text-anchor="end">{{ tick }}</text>{% endfor %}
|
||||
{% for series in chart.paths[:2] %}<path class="chart-line {{ series.key }}" d="{{ series.path }}"/>{% for x,y in series.coordinates %}<circle class="chart-point {{ series.key }}" cx="{{ x }}" cy="{{ y }}" r="3"/>{% endfor %}{% endfor %}
|
||||
<text x="48" y="270">{{ selected.start.strftime('%d %b %Y') }}</text><text x="752" y="270" text-anchor="end">{{ selected.end.strftime('%d %b %Y') }}</text></svg>
|
||||
<div class="chart-caption"><p>{{ 'Daily' if chart.bucket_days == 1 else chart.bucket_days|string ~ '-day' }} averages · {{ timezone_name }}. Gaps without readings are joined.</p>{% if view == 'overview' %}<a class="text-link" href="{{ url_for('main.dashboard', view='trends', **selected.params) }}">Explore trends →</a>{% endif %}</div>
|
||||
{% if view == 'trends' %}
|
||||
<details class="chart-details"><summary>View pulse trend · average {{ stats.pulse }} bpm</summary><svg class="trend-chart" viewBox="0 0 800 280" role="img" aria-labelledby="pulse-title"><title id="pulse-title">Pulse in bpm, {{ chart.bucket_days }}-day averages. Average {{ stats.pulse }} bpm; values available in the chart data table.</title>{% for tick in [50, 100, 150, 200] %}{% set y = 244 - tick / 260 * 208 %}<line class="grid-line" x1="48" x2="752" y1="{{ y }}" y2="{{ y }}"/><text x="34" y="{{ y + 4 }}" text-anchor="end">{{ tick }}</text>{% endfor %}<path class="chart-line" d="{{ chart.paths[2].path }}"/>{% for x,y in chart.paths[2].coordinates %}<circle class="chart-point" cx="{{ x }}" cy="{{ y }}" r="3"/>{% endfor %}<text x="48" y="270">{{ selected.start.strftime('%d %b %Y') }}</text><text x="752" y="270" text-anchor="end">{{ selected.end.strftime('%d %b %Y') }}</text></svg></details>
|
||||
<details class="chart-details"><summary>View chart data & pulse</summary><p class="muted small">Averages are calculated from original readings. Each row covers up to {{ chart.bucket_days }} day(s); pulse is shown in bpm.</p><div class="table-scroll" tabindex="0" role="region" aria-label="Chart data"><table><thead><tr><th scope="col">Period</th><th scope="col">Systolic</th><th scope="col">Diastolic</th><th scope="col">Pulse</th><th scope="col">Readings</th></tr></thead><tbody>{% for point in chart.points %}<tr><th scope="row">{{ point.date }}{% if point.date != point.end %} – {{ point.end }}{% endif %}</th><td>{{ point.systolic }}</td><td>{{ point.diastolic }}</td><td>{{ point.pulse }}</td><td>{{ point.count }}</td></tr>{% endfor %}</tbody></table></div></details>
|
||||
{% endif %}
|
||||
{% else %}<div class="empty-state"><h3>A trend begins with a reading</h3><p>No readings in this period. Try another range or add your first reading.</p></div>{% endif %}
|
||||
</section>
|
||||
@@ -1,72 +1,9 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto p-4 relative">
|
||||
<!-- Cancel Button (Top-Left) -->
|
||||
<a href="{{ request.referrer if request.referrer else url_for('main.dashboard') }}"
|
||||
class="absolute top-5 left-4 flex items-center text-gray-600 hover:text-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold mb-4 text-center">Profile Settings</h1>
|
||||
|
||||
<div class="bg-white p-6 rounded-lg shadow-md mb-6">
|
||||
|
||||
<div class="flex items-center justify-center mb-4">
|
||||
<img src="{{ url_for('user.profile_image', user_id=current_user.id) }}" alt="Profile Picture"
|
||||
class="w-32 h-32 rounded-full border object-cover shadow">
|
||||
</div>
|
||||
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.name.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.name(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.email.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.email(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.profile_pic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.profile_pic(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.systolic_threshold.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.systolic_threshold(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.diastolic_threshold.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.diastolic_threshold(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.timezone.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.timezone(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex items-center">
|
||||
{{ form.dark_mode }}
|
||||
{{ form.dark_mode.label(class="ml-2 text-sm font-medium text-gray-700") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% from 'components.html' import field %}
|
||||
{% block title %}Settings · BP Tracker{% endblock %}
|
||||
{% block content %}<div class="form-page settings-page"><div class="page-heading"><div><p class="eyebrow">MAKE IT YOURS</p><h1>Settings</h1><p class="muted">Your profile, display and personal thresholds.</p></div></div><form method="post" enctype="multipart/form-data" class="card entry-form" novalidate>{{ form.hidden_tag() }}
|
||||
{% if form.errors %}<div class="notice notice-error" role="alert">Please check the highlighted fields.</div>{% endif %}
|
||||
<fieldset><legend>Profile</legend>{% if profile.profile_pic %}<img class="profile-photo" src="{{ url_for('user.profile_image', user_id=current_user.id) }}" width="64" height="64" alt="Your profile picture">{% endif %}{{ field(form.name, autocomplete='name') }}{{ field(form.email, autocomplete='email') }}{{ field(form.profile_pic) }}</fieldset>
|
||||
<fieldset><legend>Personal thresholds</legend><p class="muted small">Readings at or above either value are labelled in History. These are your saved reference values.</p><div class="two-columns">{{ field(form.systolic_threshold, true) }}{{ field(form.diastolic_threshold, true) }}</div></fieldset>
|
||||
<fieldset><legend>Display</legend>{{ field(form.timezone) }}<div class="checkbox-field">{{ form.dark_mode }}{{ form.dark_mode.label }}</div></fieldset>
|
||||
<div class="form-actions"><button class="button primary" type="submit">Save settings</button><a href="{{ url_for('main.dashboard') }}">Back to overview</a></div></form></div>{% endblock %}
|
||||
|
||||
@@ -1,74 +1,15 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-md relative">
|
||||
<!-- Cancel Button (Top-Left) -->
|
||||
<a href="{{ request.referrer if request.referrer else url_for('main.dashboard') }}"
|
||||
class="absolute top-5 left-4 flex items-center text-gray-600 hover:text-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</a>
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Add Reading</h1>
|
||||
<form method="POST" action="{{ url_for('reading.add_reading') }}" novalidate class="space-y-6">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Timestamp Field -->
|
||||
<div>
|
||||
{{ form.timestamp.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.timestamp(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.timestamp.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Systolic Field -->
|
||||
<div>
|
||||
{{ form.systolic.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.systolic(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.systolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Diastolic Field -->
|
||||
<div>
|
||||
{{ form.diastolic.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.diastolic(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.diastolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Field -->
|
||||
<div>
|
||||
{{ form.heart_rate.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.heart_rate(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.heart_rate.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex justify-between items-center space-x-6">
|
||||
<!-- Cancel Button -->
|
||||
<a href="{{ url_for('main.dashboard') }}" class="w-full bg-gray-100 text-gray-700 py-3 rounded-lg font-semibold
|
||||
hover:bg-gray-200 hover:text-gray-900 shadow-md text-center transition">
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<!-- Save Button -->
|
||||
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold
|
||||
hover:bg-blue-700 shadow-lg text-center transition">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% from 'components.html' import field %}
|
||||
{% block title %}{{ 'Edit' if reading is defined else 'Add' }} reading · BP Tracker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form-page"><a class="back-link" href="{{ url_for('main.dashboard') }}">← Back to overview</a><div class="page-heading"><div><p class="eyebrow">ONE READING AT A TIME</p><h1>{{ 'Edit reading' if reading is defined else 'Add reading' }}</h1><p class="muted">Enter the values shown on your monitor.</p></div></div>
|
||||
<form method="post" class="card entry-form" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
{% if form.errors %}<div class="notice notice-error" role="alert">Please check the highlighted fields.{% if form.csrf_token is defined %}{% for error in form.csrf_token.errors %} {{ error }}{% endfor %}{% endif %}</div>{% endif %}
|
||||
<fieldset><legend>Blood pressure <span class="muted small">mmHg</span></legend><div class="pressure-fields">{{ field(form.systolic, true) }}<span class="entry-slash" aria-hidden="true">/</span>{{ field(form.diastolic, true) }}</div></fieldset>
|
||||
{{ field(form.heart_rate, true) }}
|
||||
<div class="timestamp-field">{{ field(form.timestamp) }}<p class="muted small">Local time · {{ current_user.profile.timezone if current_user.profile else 'UTC' }}</p></div>
|
||||
<div class="form-actions"><button class="button primary" name="action" value="save" type="submit">Save reading</button>{% if reading is not defined %}<button class="button" name="action" value="another" type="submit">Save & add another</button>{% endif %}<a href="{{ url_for('main.dashboard') }}">Cancel</a></div>
|
||||
{% if reading is defined %}<div class="delete-link"><a href="{{ url_for('reading.confirm_delete', reading_id=reading.id) }}">Delete this reading</a></div>{% endif %}
|
||||
</form><p class="form-hint">Your readings stay editable in History.</p></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,40 +1,3 @@
|
||||
{% extends "_layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto p-4 relative">
|
||||
<!-- Cancel Button (Top-Left) -->
|
||||
<a href="{{ request.referrer if request.referrer else url_for('main.dashboard') }}"
|
||||
class="absolute top-5 left-4 flex items-center text-gray-600 hover:text-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</a>
|
||||
<h2 class="text-2xl font-bold mb-4 text-center">Confirm Deletion</h2>
|
||||
<div class="bg-white p-8 rounded-lg shadow-md">
|
||||
<p class="text-sm text-gray-600 mt-2">Are you sure you want to delete the following reading?</p>
|
||||
|
||||
<!-- Reading Details -->
|
||||
<div class="mt-4 p-4 bg-gray-100 rounded">
|
||||
<p><strong>Timestamp:</strong> {{ reading.timestamp.strftime('%d %b %Y, %I:%M %p') }}</p>
|
||||
<p><strong>Systolic:</strong> {{ reading.systolic }} mmHg</p>
|
||||
<p><strong>Diastolic:</strong> {{ reading.diastolic }} mmHg</p>
|
||||
<p><strong>Heart Rate:</strong> {{ reading.heart_rate }} bpm</p>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Buttons -->
|
||||
<div class="mt-6 flex justify-end space-x-2">
|
||||
<a href="{{ url_for('main.dashboard') }}"
|
||||
class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400">
|
||||
Cancel
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('reading.delete_reading', reading_id=reading.id) }}">
|
||||
<button type="submit" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
|
||||
Confirm
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends '_layout.html' %}
|
||||
{% block title %}Delete reading · BP Tracker{% endblock %}
|
||||
{% block content %}<div class="form-page"><div class="page-heading"><div><p class="eyebrow">READING DETAILS</p><h1>Delete this reading?</h1><p class="muted">This permanently removes the reading from your history.</p></div></div><section class="card"><p class="big-number">{{ reading.systolic }} / {{ reading.diastolic }} <span class="unit">mmHg</span></p><p>{{ reading.heart_rate }} bpm · {{ reading.local_timestamp.strftime('%d %b %Y, %I:%M %p') }}</p><form method="post" action="{{ url_for('reading.delete_reading', reading_id=reading.id) }}" class="form-actions">{{ form.hidden_tag() }}<button class="button danger" type="submit">Delete reading</button><a class="button" href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}">Keep reading</a></form></section></div>{% endblock %}
|
||||
|
||||
@@ -1,79 +1 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto p-4 relative">
|
||||
<!-- Cancel Button (Top-Left) -->
|
||||
<a href="{{ url_for('main.dashboard') }}"
|
||||
class="absolute top-5 left-4 flex items-center text-gray-600 hover:text-gray-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back</span>
|
||||
</a>
|
||||
|
||||
<!-- Delete Button (Top-Right) -->
|
||||
<a href="{{ url_for('reading.confirm_delete', reading_id=reading.id) }}"
|
||||
class="absolute top-4 right-4 text-red-500 hover:text-red-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
|
||||
<h1 class="text-2xl font-bold mb-4 text-center">Edit Reading</h1>
|
||||
<form method="POST" action="{{ url_for('reading.edit_reading', reading_id=reading.id) }}" novalidate
|
||||
class="bg-white p-8 rounded-lg shadow-md">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Timestamp Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.timestamp.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.timestamp(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.timestamp.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Systolic Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.systolic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.systolic(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.systolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Diastolic Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.diastolic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.diastolic(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.diastolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.heart_rate.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.heart_rate(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.heart_rate.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="mt-6">
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends 'reading/add_reading.html' %}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends '_layout.html' %}
|
||||
{% block title %}Printable summary · BP Tracker{% endblock %}
|
||||
{% block content %}<div class="page-heading"><div><p class="eyebrow">YOUR RECORD</p><h1>Blood pressure summary</h1><p class="muted">{{ selected.start.strftime('%d %b %Y') }} – {{ selected.end.strftime('%d %b %Y') }} · {{ timezone_name }}</p><p>{{ current_user.profile.name or current_user.username }}</p></div><button class="button primary print-action" type="button" data-print hidden>Print / Save PDF</button></div><p class="muted small no-print">Use your browser’s Print command to print this report or save it as a PDF.</p><section class="card report"><div class="section-heading"><h2>{{ stats.count }} readings</h2>{% if stats.count %}<p>Average <strong>{{ stats.systolic }} / {{ stats.diastolic }} mmHg</strong> · {{ stats.pulse }} bpm</p>{% endif %}</div><div class="table-scroll"><table><thead><tr><th scope="col">Local date & time</th><th scope="col">Systolic (mmHg)</th><th scope="col">Diastolic (mmHg)</th><th scope="col">Pulse (bpm)</th></tr></thead><tbody>{% for reading in readings %}<tr><td>{{ reading.local_timestamp.strftime('%d %b %Y, %I:%M %p %Z') }}</td><td>{{ reading.systolic }}</td><td>{{ reading.diastolic }}</td><td>{{ reading.heart_rate }}</td></tr>{% else %}<tr><td colspan="4">No readings in this period.</td></tr>{% endfor %}</tbody></table></div></section>{% endblock %}
|
||||
Reference in New Issue
Block a user