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
This commit is contained in:
@@ -6,17 +6,12 @@
|
||||
<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="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="/static/css/tailwind.css" rel="stylesheet">
|
||||
<script src="/static/js/alpine.min.js" defer></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="/static/js/turbolinks.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 text-gray-800 font-sans antialiased">
|
||||
<nav class="flex items-center justify-between flex-wrap p-6 fixed w-full z-10 top-0 transition-colors duration-300 shadow-md"
|
||||
x-data="{ isOpen: false }" @keydown.escape="isOpen = false" @click.away="isOpen = false"
|
||||
:class="{ 'bg-primary-900' : isOpen , 'bg-primary-800' : !isOpen}">
|
||||
<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="/">
|
||||
@@ -25,20 +20,18 @@
|
||||
</div>
|
||||
|
||||
<!--Toggle button (hidden on large screens)-->
|
||||
<button @click="isOpen = !isOpen" type="button"
|
||||
class="block lg:hidden px-2 text-gray-500 hover:text-white focus:outline-none focus:text-white"
|
||||
:class="{ 'transition transform-180': isOpen }">
|
||||
<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 x-show="isOpen" fill-rule="evenodd" clip-rule="evenodd"
|
||||
<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 x-show="!isOpen" fill-rule="evenodd"
|
||||
<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 class="w-full flex-grow lg:flex lg:items-center lg:w-auto"
|
||||
:class="{ 'block shadow-3xl': isOpen, 'hidden': !isOpen }" x-show.transition="true">
|
||||
<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">
|
||||
@@ -121,11 +114,11 @@
|
||||
{% if messages %}
|
||||
<div class="fixed top-24 right-4 z-50 space-y-4">
|
||||
{% for category, message in messages %}
|
||||
<div class="flex items-center justify-between p-4 rounded-xl shadow-xl text-white bg-{{ 'red' if category == 'danger' else 'primary' }}-500 min-w-[300px]"
|
||||
x-data="{ visible: true }" x-show="visible" x-transition.duration.300ms>
|
||||
<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 @click="visible = false"
|
||||
class="text-2xl font-bold ml-4 hover:text-gray-200 transition-colors">×</button>
|
||||
<button
|
||||
class="flash-close-btn text-2xl font-bold ml-4 hover:text-gray-200 transition-colors">×</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -146,6 +139,76 @@
|
||||
</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>
|
||||
@@ -40,20 +40,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-data="{ open: {{ 'true' if request.method == 'POST' else 'false' }} }" class="relative">
|
||||
<div class="relative">
|
||||
<!-- Compact Icon -->
|
||||
<button @click="open = !open"
|
||||
class="bg-primary-600 text-white p-3 rounded-full shadow-lg focus:outline-none hover:bg-primary-700">
|
||||
<button id="filter-btn"
|
||||
class="bg-primary-600 text-white p-3 rounded-full shadow-lg focus:outline-none hover:bg-primary-700 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="w-6 h-6">
|
||||
<path x-show="!open" stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
|
||||
<path x-show="open" x-cloak stroke-linecap="round" stroke-linejoin="round" d="M18 15l-6-6-6 6" />
|
||||
<!-- Chevron down icon (default) -->
|
||||
<path id="filter-icon-closed" class="{{ 'hidden' if request.method == 'POST' else '' }}"
|
||||
stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
|
||||
<!-- X icon -->
|
||||
<path id="filter-icon-open" class="{{ '' if request.method == 'POST' else 'hidden' }}"
|
||||
stroke-linecap="round" stroke-linejoin="round" d="M18 15l-6-6-6 6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Collapsible Filter Form -->
|
||||
<div x-show="open" x-transition.duration.300ms
|
||||
class="w-full md:w-1/3 bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||
<div id="filter-form"
|
||||
class="{{ '' if request.method == 'POST' else 'hidden' }} transition-all duration-300 w-full md:w-1/3 bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-800">Filter Readings</h3>
|
||||
</div>
|
||||
@@ -84,22 +88,21 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="max-w-5xl mx-auto" x-data="{ activeView: '{{ active_view }}' }">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b mb-4">
|
||||
<button @click="activeView = 'list'" hx-get="{{ url_for('main.dashboard_list') }}"
|
||||
hx-target="#dashboard-content" :class="{'border-primary-600 text-primary-600': activeView === 'list'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">List View</button>
|
||||
<button @click="activeView = 'weekly'" hx-get="{{ url_for('main.dashboard_weekly') }}"
|
||||
hx-target="#dashboard-content" :class="{'border-primary-600 text-primary-600': activeView === 'weekly'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">Weekly View</button>
|
||||
<button @click="activeView = 'monthly'" hx-get="{{ url_for('main.dashboard_monthly') }}"
|
||||
hx-target="#dashboard-content"
|
||||
:class="{'border-primary-600 text-primary-600': activeView === 'monthly'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">Monthly View</button>
|
||||
<button @click="activeView = 'graph'" hx-get="{{ url_for('main.dashboard_graph') }}"
|
||||
hx-target="#dashboard-content" :class="{'border-primary-600 text-primary-600': activeView === 'graph'}"
|
||||
class="px-4 py-2 text-sm font-medium border-b-2">Graph View</button>
|
||||
<div class="flex border-b mb-4" 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 {{ 'border-primary-600 text-primary-600' if active_view == 'list' else '' }}">List
|
||||
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 {{ 'border-primary-600 text-primary-600' if active_view == 'weekly' else '' }}">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 {{ 'border-primary-600 text-primary-600' if active_view == 'monthly' else '' }}">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 {{ 'border-primary-600 text-primary-600' if active_view == 'graph' else '' }}">Graph
|
||||
View</button>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Content Target Area for HTMX -->
|
||||
@@ -109,4 +112,40 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Filter Form Toggle
|
||||
const filterBtn = document.getElementById('filter-btn');
|
||||
const filterForm = document.getElementById('filter-form');
|
||||
const iconClosed = document.getElementById('filter-icon-closed');
|
||||
const iconOpen = document.getElementById('filter-icon-open');
|
||||
|
||||
if (filterBtn && filterForm) {
|
||||
filterBtn.addEventListener('click', () => {
|
||||
const isHidden = filterForm.classList.contains('hidden');
|
||||
if (isHidden) {
|
||||
filterForm.classList.remove('hidden');
|
||||
iconClosed.classList.add('hidden');
|
||||
iconOpen.classList.remove('hidden');
|
||||
} else {
|
||||
filterForm.classList.add('hidden');
|
||||
iconClosed.classList.remove('hidden');
|
||||
iconOpen.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Tabs
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
tabBtns.forEach(b => {
|
||||
b.classList.remove('border-primary-600', 'text-primary-600');
|
||||
});
|
||||
btn.classList.add('border-primary-600', 'text-primary-600');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,3 +1,10 @@
|
||||
<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) }}"
|
||||
@@ -8,8 +15,7 @@
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 8v4l3 3m9-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
||||
<use href="#icon-clock"></use>
|
||||
</svg>
|
||||
<span title="{{ reading.local_timestamp.strftime('%d %b %Y, %I:%M %p') }}">
|
||||
{{ reading.relative_timestamp }}
|
||||
@@ -34,7 +40,7 @@
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
<use href="#icon-chevron"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.7 KiB |
Reference in New Issue
Block a user