Files
bloodpressure/app/templates/_layout.html
Peter Stockings dcef99c3bf perf: massive reduction in application bundle size
- Replaced Alpine.js and Turbolinks with lightweight vanilla JS event listeners
- Swapped HTMX CDN import for a custom 15-line native JS polyfill
- Removed Google Fonts ('Inter') in favor of the native system font stack
- Extracted repeated SVGs in list views into a `<defs>` block to shrink HTML
- Reduced dashboard pagination PAGE_SIZE from 50 to 25
- Minified Tailwind CSS output and enabled Gzip/Brotli via Flask-Compress
2026-03-10 19:10:47 +11:00

214 lines
10 KiB
HTML

<!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">
</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') }}">
{% if current_user.profile and current_user.profile.profile_pic %}
<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">
{% else %}
<!-- Default SVG Icon -->
<div
class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center border-2 border-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-5 h-5 text-gray-600"
viewBox="0 0 24 24">
<path
d="M12 12c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
</svg>
</div>
{% endif %}
<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">&times;</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>&copy; 2024 BP Tracker. All rights reserved.</p>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Flash messages
document.querySelectorAll('.flash-close-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const el = e.target.closest('.flash-message');
if (el) {
el.style.opacity = '0';
setTimeout(() => el.remove(), 300);
}
});
});
// Micro-HTMX implementation
document.body.addEventListener('click', async (e) => {
const trigger = e.target.closest('[hx-get]');
if (!trigger) return;
e.preventDefault();
const url = trigger.getAttribute('hx-get');
const targetSelector = trigger.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);
}
});
// Mobile Menu
const menuBtn = document.getElementById('mobile-menu-btn');
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 (menuBtn && menu) {
const toggleMenu = (forceClose = false) => {
const isHidden = menu.classList.contains('hidden');
if (!isHidden || forceClose) {
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');
}
};
menuBtn.addEventListener('click', () => toggleMenu());
document.addEventListener('click', (e) => {
if (!nav.contains(e.target) && !menu.classList.contains('hidden')) toggleMenu(true);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !menu.classList.contains('hidden')) toggleMenu(true);
});
}
});
</script>
</body>
</html>