Redesign BP tracker with lightweight views and unobtrusive save confirmations

This commit is contained in:
Peter Stockings
2026-09-08 15:07:57 +10:00
parent 25d1774e53
commit 87b60aac9b
40 changed files with 2693 additions and 2553 deletions
-346
View File
@@ -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">&laquo; 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 &raquo;</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>
-172
View File
@@ -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">&laquo;
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
&raquo;</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>
+4
View File
@@ -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>
+15
View File
@@ -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>