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>
|
||||
|
||||
Reference in New Issue
Block a user