diff --git a/app/routes.py b/app/routes.py index a5204b0..15a7100 100644 --- a/app/routes.py +++ b/app/routes.py @@ -2,6 +2,7 @@ import csv from io import StringIO import io from flask import Blueprint, Response, make_response, render_template, redirect, request, send_file, url_for, flash +import humanize from sqlalchemy import func from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.http import http_date @@ -104,6 +105,12 @@ def dashboard(): # Fetch and order readings readings = readings_query.order_by(Reading.timestamp.desc()).all() + # Add human-readable relative timestamps to readings + now = datetime.utcnow() + for reading in readings: + reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp) + + # Weekly summary (last 7 days) one_week_ago = datetime.now() - timedelta(days=7) weekly_readings = [r for r in readings if r.timestamp >= one_week_ago] diff --git a/app/static/css/tailwind.css b/app/static/css/tailwind.css index d591e88..a21c32d 100644 --- a/app/static/css/tailwind.css +++ b/app/static/css/tailwind.css @@ -600,14 +600,54 @@ video { position: fixed; } +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.inset-0 { + inset: 0px; +} + .top-0 { top: 0px; } +.right-2 { + right: 0.5rem; +} + +.top-2 { + top: 0.5rem; +} + +.bottom-2 { + bottom: 0.5rem; +} + +.left-4 { + left: 1rem; +} + +.right-4 { + right: 1rem; +} + +.top-4 { + top: 1rem; +} + .z-10 { z-index: 10; } +.col-span-full { + grid-column: 1 / -1; +} + .mx-auto { margin-left: auto; margin-right: auto; @@ -810,6 +850,10 @@ video { grid-template-columns: repeat(1, minmax(0, 1fr)); } +.flex-col { + flex-direction: column; +} + .flex-wrap { flex-wrap: wrap; } @@ -842,6 +886,10 @@ video { gap: 2rem; } +.gap-6 { + gap: 1.5rem; +} + .space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); @@ -896,6 +944,10 @@ video { border-radius: 0.5rem; } +.rounded-md { + border-radius: 0.375rem; +} + .border { border-width: 1px; } @@ -1066,6 +1118,11 @@ video { padding-bottom: 1rem; } +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + .pl-2 { padding-left: 0.5rem; } @@ -1117,6 +1174,11 @@ video { line-height: 1.75rem; } +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + .font-bold { font-weight: 700; } @@ -1173,6 +1235,21 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity, 1)); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity, 1)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity, 1)); +} + .no-underline { text-decoration-line: none; } @@ -1209,6 +1286,18 @@ video { transition-duration: 150ms; } +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-shadow { + transition-property: box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .hover\:bg-blue-700:hover { --tw-bg-opacity: 1; background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); @@ -1269,6 +1358,21 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity, 1)); } +.hover\:text-gray-600:hover { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity, 1)); +} + +.hover\:text-gray-800:hover { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity, 1)); +} + +.hover\:text-red-700:hover { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity, 1)); +} + .hover\:underline:hover { text-decoration-line: underline; } @@ -1277,6 +1381,12 @@ video { text-decoration-line: none; } +.hover\:shadow-lg:hover { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .focus\:border-blue-500:focus { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)); @@ -1287,6 +1397,11 @@ video { border-color: rgb(156 163 175 / var(--tw-border-opacity, 1)); } +.focus\:border-red-500:focus { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity, 1)); +} + .focus\:text-white:focus { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity, 1)); @@ -1360,6 +1475,10 @@ video { width: auto; } + .lg\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .lg\:items-center { align-items: center; } diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 7fdefc6..602a2a3 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -168,67 +168,58 @@
- - - - - - - - - - - - - {% for reading in readings %} - - - - - - - - - - - {% else %} - - - - {% endfor %} - -
TimestampBlood Pressure (mmHg)Heart RateActions
- {{ reading.timestamp.strftime('%d %b %Y, %I:%M %p') }} - - {{ reading.systolic }}/{{ reading.diastolic }} - - {{ reading.heart_rate }} bpm - - - - - - + - Edit - - - - - - - Delete - -
- No readings found. -
diff --git a/app/templates/edit_reading.html b/app/templates/edit_reading.html index 1ab3342..522e183 100644 --- a/app/templates/edit_reading.html +++ b/app/templates/edit_reading.html @@ -1,6 +1,27 @@ {% extends "_layout.html" %} {% block content %} -
+
+ + + + + + Back + + + + + + + + + +

Edit Reading

{{ form.hidden_tag() }} @@ -45,17 +66,10 @@ {% endfor %}
- -
- - - Cancel - - - + +
diff --git a/requirements.txt b/requirements.txt index 09e9bab..c0d8f5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ flask-sqlalchemy==3.1.1 flask-wtf==1.2.2 greenlet==3.1.1 gunicorn==23.0.0 +humanize==4.11.0 idna==3.10 importlib-metadata==8.5.0 itsdangerous==2.2.0