Get project working locally
This commit is contained in:
40
app/__init__.py
Normal file
40
app/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_login import LoginManager
|
||||
|
||||
# Initialize Flask extensions
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
bcrypt = Bcrypt()
|
||||
login_manager = LoginManager()
|
||||
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message_category = 'info'
|
||||
|
||||
def create_app(config_class='app.config.DevelopmentConfig'):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
bcrypt.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
|
||||
# Import models here to avoid circular imports
|
||||
from app.models import User # Import the User model
|
||||
|
||||
# Set up the user_loader function
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id)) # Query the User model by ID
|
||||
|
||||
# Register blueprints
|
||||
from app.routes import main, auth, user
|
||||
app.register_blueprint(main, url_prefix='/')
|
||||
app.register_blueprint(auth, url_prefix='/auth')
|
||||
app.register_blueprint(user, url_prefix='/user')
|
||||
|
||||
return app
|
||||
31
app/config.py
Normal file
31
app/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
"""Base configuration."""
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', '234234sdfsdfsdfsdf345345')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""Development configuration."""
|
||||
DEBUG = True
|
||||
|
||||
uri = os.environ.get('DATABASE_URL', 'postgresql://postgres:59fff56880e1bbb42e753d2a82ac21b6@peterstockings.com:15389/bloodpressure_db')
|
||||
if uri and uri.startswith("postgres://"):
|
||||
uri = uri.replace("postgres://", "postgresql://", 1)
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'postgresql://postgres:59fff56880e1bbb42e753d2a82ac21b6@peterstockings.com:15389/bloodpressure_db')
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production configuration."""
|
||||
DEBUG = False
|
||||
|
||||
uri = os.environ.get('DATABASE_URL', 'postgresql://postgres:59fff56880e1bbb42e753d2a82ac21b6@peterstockings.com:15389/bloodpressure_db')
|
||||
if uri and uri.startswith("postgres://"):
|
||||
uri = uri.replace("postgres://", "postgresql://", 1)
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = uri
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""Testing configuration."""
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
|
||||
100
app/forms.py
Normal file
100
app/forms.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from typing import Optional
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, FileField, StringField, PasswordField, SubmitField, IntegerField, DateTimeLocalField
|
||||
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Email, Optional, NumberRange
|
||||
from app.models import User
|
||||
from datetime import datetime
|
||||
|
||||
class SignupForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Username',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
Length(min=4, max=20, message="Username must be between 4 and 20 characters.")
|
||||
]
|
||||
)
|
||||
password = PasswordField(
|
||||
'Password',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
Length(min=6, message="Password must be at least 6 characters long.")
|
||||
]
|
||||
)
|
||||
confirm_password = PasswordField(
|
||||
'Confirm Password',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
EqualTo('password', message="Passwords must match.")
|
||||
]
|
||||
)
|
||||
submit = SubmitField('Sign Up')
|
||||
|
||||
# Custom validator to check if username is already taken
|
||||
def validate_username(self, username):
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user:
|
||||
raise ValidationError("Username is already taken. Please choose a different one.")
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Username',
|
||||
validators=[DataRequired(message="Username is required.")]
|
||||
)
|
||||
password = PasswordField(
|
||||
'Password',
|
||||
validators=[DataRequired(message="Password is required.")]
|
||||
)
|
||||
submit = SubmitField('Login')
|
||||
|
||||
|
||||
class ReadingForm(FlaskForm):
|
||||
timestamp = DateTimeLocalField(
|
||||
'Timestamp',
|
||||
format='%Y-%m-%dT%H:%M',
|
||||
default=datetime.now, # Set default to current time
|
||||
validators=[DataRequired(message="Timestamp is required.")]
|
||||
)
|
||||
systolic = IntegerField(
|
||||
'Systolic (mmHg)',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
NumberRange(min=50, max=250, message="Systolic pressure must be between 50 and 250 mmHg.")
|
||||
]
|
||||
)
|
||||
diastolic = IntegerField(
|
||||
'Diastolic (mmHg)',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
NumberRange(min=30, max=150, message="Diastolic pressure must be between 30 and 150 mmHg.")
|
||||
]
|
||||
)
|
||||
heart_rate = IntegerField(
|
||||
'Heart Rate (bpm)',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
NumberRange(min=30, max=200, message="Heart rate must be between 30 and 200 bpm.")
|
||||
]
|
||||
)
|
||||
submit = SubmitField('Save Reading')
|
||||
|
||||
class DeleteForm(FlaskForm):
|
||||
submit = SubmitField('Delete')
|
||||
|
||||
class ProfileForm(FlaskForm):
|
||||
name = StringField('Name', validators=[Optional()])
|
||||
email = StringField('Email', validators=[Optional(), Email()])
|
||||
profile_pic = FileField('Profile Picture (optional)')
|
||||
systolic_threshold = IntegerField(
|
||||
'Systolic Threshold (mmHg)',
|
||||
validators=[Optional(), NumberRange(min=90, max=200)]
|
||||
)
|
||||
diastolic_threshold = IntegerField(
|
||||
'Diastolic Threshold (mmHg)',
|
||||
validators=[Optional(), NumberRange(min=60, max=120)]
|
||||
)
|
||||
dark_mode = BooleanField('Enable Dark Mode')
|
||||
submit = SubmitField('Save Settings')
|
||||
|
||||
|
||||
|
||||
29
app/models.py
Normal file
29
app/models.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
|
||||
from app import db
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(255), nullable=False, unique=True)
|
||||
password_hash = db.Column(db.Text, nullable=False)
|
||||
profile = db.relationship('Profile', backref='user', uselist=False)
|
||||
|
||||
class Profile(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
name = db.Column(db.String(100))
|
||||
email = db.Column(db.String(150), unique=True)
|
||||
profile_pic = db.Column(db.Text) # Store image as a base64 string
|
||||
systolic_threshold = db.Column(db.Integer, default=140)
|
||||
diastolic_threshold = db.Column(db.Integer, default=90)
|
||||
dark_mode = db.Column(db.Boolean, default=False)
|
||||
|
||||
class Reading(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
timestamp = db.Column(db.DateTime, nullable=False)
|
||||
systolic = db.Column(db.Integer, nullable=False)
|
||||
diastolic = db.Column(db.Integer, nullable=False)
|
||||
heart_rate = db.Column(db.Integer, nullable=False)
|
||||
user = db.relationship('User', backref='readings')
|
||||
176
app/routes.py
Normal file
176
app/routes.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from flask import Blueprint, render_template, redirect, request, url_for, flash
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app.models import Profile, Reading, db, User
|
||||
from app.forms import DeleteForm, LoginForm, ProfileForm, ReadingForm, SignupForm
|
||||
from flask_login import login_user, login_required, current_user, logout_user
|
||||
import base64
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
auth = Blueprint('auth', __name__)
|
||||
user = Blueprint('user', __name__)
|
||||
|
||||
|
||||
@auth.route('/signup', methods=['GET', 'POST'])
|
||||
def signup():
|
||||
form = SignupForm()
|
||||
if form.validate_on_submit():
|
||||
hashed_password = generate_password_hash(form.password.data)
|
||||
new_user = User(username=form.username.data, password_hash=hashed_password)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
flash("Account created successfully. Please log in.", "success")
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('signup.html', form=form)
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user and check_password_hash(user.password_hash, form.password.data):
|
||||
login_user(user)
|
||||
flash("Logged in successfully.", "success")
|
||||
return redirect(url_for('main.dashboard'))
|
||||
flash("Invalid username or password.", "danger")
|
||||
return render_template('login.html', form=form)
|
||||
|
||||
@auth.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user() # Logs out the current user
|
||||
flash('You have been logged out.', 'success')
|
||||
return redirect(url_for('auth.login')) # Redirect to login page or home page
|
||||
|
||||
|
||||
@main.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
from datetime import datetime, timedelta
|
||||
profile = current_user.profile
|
||||
|
||||
# Get all readings for the user
|
||||
readings = Reading.query.filter_by(user_id=current_user.id).order_by(Reading.timestamp.desc()).all()
|
||||
|
||||
# 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]
|
||||
systolic_avg = round(sum(r.systolic for r in weekly_readings) / len(weekly_readings), 1) if weekly_readings else 0
|
||||
diastolic_avg = round(sum(r.diastolic for r in weekly_readings) / len(weekly_readings), 1) if weekly_readings else 0
|
||||
heart_rate_avg = round(sum(r.heart_rate for r in weekly_readings) / len(weekly_readings), 1) if weekly_readings else 0
|
||||
|
||||
# Progress badges
|
||||
badges = []
|
||||
if len(readings) >= 10:
|
||||
badges.append("10 Readings Logged")
|
||||
if len(readings) >= 100:
|
||||
badges.append("100 Readings Milestone")
|
||||
if len(weekly_readings) >= 7:
|
||||
badges.append("Logged Readings for 7 Days")
|
||||
|
||||
# Pass the delete form to the template
|
||||
delete_form = DeleteForm()
|
||||
|
||||
return render_template('dashboard.html', readings=readings, profile=profile, badges=badges,
|
||||
systolic_avg=systolic_avg, diastolic_avg=diastolic_avg, heart_rate_avg=heart_rate_avg,
|
||||
delete_form=delete_form)
|
||||
|
||||
@main.route('/dashboard/filter', methods=['POST'])
|
||||
@login_required
|
||||
def filter_dashboard():
|
||||
from datetime import datetime
|
||||
start_date = request.form.get('start_date')
|
||||
end_date = request.form.get('end_date')
|
||||
|
||||
# Parse dates and filter readings
|
||||
readings = Reading.query.filter(
|
||||
Reading.user_id == current_user.id,
|
||||
Reading.timestamp >= datetime.strptime(start_date, '%Y-%m-%d'),
|
||||
Reading.timestamp <= datetime.strptime(end_date, '%Y-%m-%d')
|
||||
).order_by(Reading.timestamp.desc()).all()
|
||||
|
||||
# Pass the delete form to the template
|
||||
delete_form = DeleteForm()
|
||||
|
||||
return render_template('dashboard.html', readings=readings, delete_form=delete_form)
|
||||
|
||||
@main.route('/add-reading', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_reading():
|
||||
form = ReadingForm()
|
||||
if form.validate_on_submit():
|
||||
new_reading = Reading(
|
||||
user_id=current_user.id,
|
||||
timestamp=form.timestamp.data,
|
||||
systolic=form.systolic.data,
|
||||
diastolic=form.diastolic.data,
|
||||
heart_rate=form.heart_rate.data
|
||||
)
|
||||
db.session.add(new_reading)
|
||||
db.session.commit()
|
||||
flash("Reading added successfully.", "success")
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return render_template('add_reading.html', form=form)
|
||||
|
||||
@main.route('/reading/<int:reading_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_reading(reading_id):
|
||||
reading = Reading.query.get_or_404(reading_id)
|
||||
|
||||
# Ensure the reading belongs to the logged-in user
|
||||
if reading.user_id != current_user.id:
|
||||
flash('You are not authorized to edit this reading.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
form = ReadingForm(obj=reading) # Populate form with existing reading data
|
||||
if form.validate_on_submit():
|
||||
reading.timestamp = form.timestamp.data
|
||||
reading.systolic = form.systolic.data
|
||||
reading.diastolic = form.diastolic.data
|
||||
reading.heart_rate = form.heart_rate.data
|
||||
db.session.commit()
|
||||
flash('Reading updated successfully!', 'success')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
return render_template('edit_reading.html', form=form, reading=reading)
|
||||
|
||||
@main.route('/reading/<int:reading_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_reading(reading_id):
|
||||
reading = Reading.query.get_or_404(reading_id)
|
||||
|
||||
# Ensure the reading belongs to the logged-in user
|
||||
if reading.user_id != current_user.id:
|
||||
flash('You are not authorized to delete this reading.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
db.session.delete(reading)
|
||||
db.session.commit()
|
||||
flash('Reading deleted successfully!', 'success')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
|
||||
@user.route('/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def profile():
|
||||
profile = current_user.profile or Profile(user_id=current_user.id)
|
||||
form = ProfileForm(obj=profile)
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Update profile fields
|
||||
profile.name = form.name.data
|
||||
profile.email = form.email.data
|
||||
profile.systolic_threshold = form.systolic_threshold.data or profile.systolic_threshold
|
||||
profile.diastolic_threshold = form.diastolic_threshold.data or profile.diastolic_threshold
|
||||
profile.dark_mode = form.dark_mode.data
|
||||
|
||||
# Handle profile picture upload
|
||||
if form.profile_pic.data:
|
||||
file_data = form.profile_pic.data.read()
|
||||
profile.profile_pic = base64.b64encode(file_data).decode('utf-8')
|
||||
|
||||
db.session.add(profile)
|
||||
db.session.commit()
|
||||
flash('Profile updated successfully!', 'success')
|
||||
return redirect(url_for('user.profile'))
|
||||
|
||||
return render_template('profile.html', form=form, profile=profile)
|
||||
63
app/static/js/tailwindcss@3.2.4.js
Normal file
63
app/static/js/tailwindcss@3.2.4.js
Normal file
File diff suppressed because one or more lines are too long
58
app/templates/_layout.html
Normal file
58
app/templates/_layout.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!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>
|
||||
<script src="/static/js/tailwindcss@3.2.4.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 text-gray-800">
|
||||
<!-- Navbar -->
|
||||
<nav class="bg-blue-600 text-white p-4">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<a href="/" class="text-2xl font-bold">BP Tracker</a>
|
||||
<div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('main.dashboard') }}" class="px-4 py-2 hover:underline">Dashboard</a>
|
||||
<a href="{{ url_for('user.profile') }}" class="px-4 py-2 hover:underline">Profile</a>
|
||||
<a href="{{ url_for('auth.logout') }}" class="px-4 py-2 bg-red-500 rounded hover:bg-red-600">Logout</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}" class="px-4 py-2 hover:underline">Login</a>
|
||||
<a href="{{ url_for('auth.signup') }}"
|
||||
class="px-4 py-2 bg-white text-blue-600 rounded hover:bg-gray-200">Signup</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
<div class="container mx-auto mt-4">
|
||||
{% for category, message in messages %}
|
||||
<div class="p-4 mb-4 rounded text-white bg-{{ 'red' if category == 'danger' else 'green' }}-500">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto mt-6">
|
||||
{% 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>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
55
app/templates/add_reading.html
Normal file
55
app/templates/add_reading.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Add Reading</h1>
|
||||
<form method="POST" action="{{ url_for('main.add_reading') }}" novalidate class="space-y-6">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Timestamp Field -->
|
||||
<div>
|
||||
{{ form.timestamp.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.timestamp(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.timestamp.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Systolic Field -->
|
||||
<div>
|
||||
{{ form.systolic.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.systolic(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.systolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Diastolic Field -->
|
||||
<div>
|
||||
{{ form.diastolic.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.diastolic(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.diastolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Field -->
|
||||
<div>
|
||||
{{ form.heart_rate.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.heart_rate(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.heart_rate.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-gradient-to-r from-blue-500 to-blue-700 text-white py-3 rounded-lg
|
||||
font-semibold hover:from-blue-600 hover:to-blue-800 shadow-lg") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
106
app/templates/dashboard.html
Normal file
106
app/templates/dashboard.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-5xl mx-auto p-4 space-y-6">
|
||||
|
||||
<!-- Header Section with "Add New Reading" Button -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-800">Dashboard</h1>
|
||||
<a href="{{ url_for('main.add_reading') }}"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded shadow hover:bg-blue-700">
|
||||
+ Add New Reading
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Summary -->
|
||||
<div class="bg-gradient-to-r from-blue-500 to-blue-700 text-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-lg font-bold">Weekly Summary</h3>
|
||||
<div class="flex justify-between mt-4">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Systolic Average</p>
|
||||
<p class="text-2xl">{{ systolic_avg }} <span class="text-base">mmHg</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Diastolic Average</p>
|
||||
<p class="text-2xl">{{ diastolic_avg }} <span class="text-base">mmHg</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Heart Rate Average</p>
|
||||
<p class="text-2xl">{{ heart_rate_avg }} <span class="text-base">bpm</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Badges -->
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-2">Progress Badges</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{% for badge in badges %}
|
||||
<div class="bg-green-100 text-green-800 px-4 py-2 rounded shadow text-sm font-medium">
|
||||
{{ badge }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Form -->
|
||||
<form method="POST" action="{{ url_for('main.filter_dashboard') }}" class="p-4 bg-white rounded-lg shadow-md">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4">Filter Readings</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="start_date" class="block text-sm font-medium text-gray-700">Start Date</label>
|
||||
<input type="date" name="start_date" id="start_date" class="w-full p-2 border rounded">
|
||||
</div>
|
||||
<div>
|
||||
<label for="end_date" class="block text-sm font-medium text-gray-700">End Date</label>
|
||||
<input type="date" name="end_date" id="end_date" class="w-full p-2 border rounded">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="w-full md:w-auto bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Readings Table -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<table class="w-full bg-white rounded shadow border-collapse">
|
||||
<thead class="bg-gray-200">
|
||||
<tr>
|
||||
<th class="p-2 text-left">Timestamp</th>
|
||||
<th class="p-2 text-left">Systolic</th>
|
||||
<th class="p-2 text-left">Diastolic</th>
|
||||
<th class="p-2 text-left">Heart Rate</th>
|
||||
<th class="p-2 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reading in readings %}
|
||||
<tr class="border-t hover:bg-gray-100">
|
||||
<td class="p-2">{{ reading.timestamp }}</td>
|
||||
<td class="p-2">{{ reading.systolic }}</td>
|
||||
<td class="p-2">{{ reading.diastolic }}</td>
|
||||
<td class="p-2">{{ reading.heart_rate }}</td>
|
||||
<td class="p-2">
|
||||
<a href="{{ url_for('main.edit_reading', reading_id=reading.id) }}"
|
||||
class="text-blue-600 hover:underline">Edit</a>
|
||||
<form method="POST" action="{{ url_for('main.delete_reading', reading_id=reading.id) }}"
|
||||
class="inline">
|
||||
{{ delete_form.hidden_tag() }}
|
||||
<button type="submit" class="text-red-600 hover:underline ml-2">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="p-2 text-center text-gray-500">No readings found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
56
app/templates/edit_reading.html
Normal file
56
app/templates/edit_reading.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Edit Reading</h1>
|
||||
<form method="POST" action="{{ url_for('main.edit_reading', reading_id=reading.id) }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Timestamp Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.timestamp.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.timestamp(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.timestamp.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Systolic Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.systolic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.systolic(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.systolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Diastolic Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.diastolic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.diastolic(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.diastolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.heart_rate.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.heart_rate(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.heart_rate.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
app/templates/login.html
Normal file
32
app/templates/login.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-4">Login</h1>
|
||||
<form method="POST" action="{{ url_for('auth.login') }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.username.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.username(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.username.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.password.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.password(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.password.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
58
app/templates/profile.html
Normal file
58
app/templates/profile.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-6">Profile Settings</h1>
|
||||
|
||||
<div class="flex items-center justify-center mb-4">
|
||||
{% if profile.profile_pic %}
|
||||
<img src="data:image/jpeg;base64,{{ profile.profile_pic }}" alt="Profile Picture"
|
||||
class="w-24 h-24 rounded-full border">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='default.png') }}" alt="Default Profile Picture"
|
||||
class="w-24 h-24 rounded-full border">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.name.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.name(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.email.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.email(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.profile_pic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.profile_pic(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.systolic_threshold.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.systolic_threshold(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.diastolic_threshold.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.diastolic_threshold(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex items-center">
|
||||
{{ form.dark_mode }}
|
||||
{{ form.dark_mode.label(class="ml-2 text-sm font-medium text-gray-700") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
app/templates/signup.html
Normal file
42
app/templates/signup.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-4">Sign Up</h1>
|
||||
<form method="POST" action="{{ url_for('auth.signup') }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.username.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.username(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.username.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.password.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.password(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.password.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.confirm_password.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.confirm_password(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
{% for error in form.confirm_password.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user