diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..0b1da6d2 Binary files /dev/null and b/.DS_Store differ diff --git a/instance/database.db b/instance/database.db new file mode 100644 index 00000000..bd147bcc Binary files /dev/null and b/instance/database.db differ diff --git a/migrations/README b/migrations/README new file mode 100644 index 00000000..0e048441 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/__pycache__/env.cpython-312.pyc b/migrations/__pycache__/env.cpython-312.pyc new file mode 100644 index 00000000..9b48b04f Binary files /dev/null and b/migrations/__pycache__/env.cpython-312.pyc differ diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 00000000..ec9d45c2 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 00000000..4c970927 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/__pycache__/d19dd2c3e751_initial_migration.cpython-312.pyc b/migrations/versions/__pycache__/d19dd2c3e751_initial_migration.cpython-312.pyc new file mode 100644 index 00000000..0b7d18c9 Binary files /dev/null and b/migrations/versions/__pycache__/d19dd2c3e751_initial_migration.cpython-312.pyc differ diff --git a/migrations/versions/d19dd2c3e751_initial_migration.py b/migrations/versions/d19dd2c3e751_initial_migration.py new file mode 100644 index 00000000..059c57ba --- /dev/null +++ b/migrations/versions/d19dd2c3e751_initial_migration.py @@ -0,0 +1,70 @@ +"""Initial migration. + +Revision ID: d19dd2c3e751 +Revises: +Create Date: 2024-07-09 18:29:09.592453 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd19dd2c3e751' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('questionnaire_response', schema=None) as batch_op: + batch_op.add_column(sa.Column('question_id', sa.String(length=50), nullable=True)) + batch_op.add_column(sa.Column('response', sa.String(length=1000), nullable=True)) + batch_op.alter_column('user_id', + existing_type=sa.INTEGER(), + nullable=True) + batch_op.drop_column('question3') + batch_op.drop_column('question2') + batch_op.drop_column('question1') + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('has_completed_questionnaire', sa.Boolean(), nullable=True)) + + with op.batch_alter_table('weight', schema=None) as batch_op: + batch_op.alter_column('user_id', + existing_type=sa.INTEGER(), + nullable=True) + + with op.batch_alter_table('weight_entry', schema=None) as batch_op: + batch_op.add_column(sa.Column('date', sa.DateTime(), nullable=True)) + batch_op.drop_column('date_added') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('weight_entry', schema=None) as batch_op: + batch_op.add_column(sa.Column('date_added', sa.DATETIME(), nullable=True)) + batch_op.drop_column('date') + + with op.batch_alter_table('weight', schema=None) as batch_op: + batch_op.alter_column('user_id', + existing_type=sa.INTEGER(), + nullable=False) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_column('has_completed_questionnaire') + + with op.batch_alter_table('questionnaire_response', schema=None) as batch_op: + batch_op.add_column(sa.Column('question1', sa.VARCHAR(length=200), nullable=False)) + batch_op.add_column(sa.Column('question2', sa.VARCHAR(length=200), nullable=False)) + batch_op.add_column(sa.Column('question3', sa.VARCHAR(length=200), nullable=False)) + batch_op.alter_column('user_id', + existing_type=sa.INTEGER(), + nullable=False) + batch_op.drop_column('response') + batch_op.drop_column('question_id') + + # ### end Alembic commands ### diff --git a/questionnaire_tree b/questionnaire_tree new file mode 100644 index 00000000..003ec201 --- /dev/null +++ b/questionnaire_tree @@ -0,0 +1,22 @@ +// Questionnaire Tree +digraph { + 1 [label="Are there any muscle groups you DO NOT want to exercise?"] + 1.1 [label="Select all muscle groups you DO NOT want to exercise"] + 2 [label="How many days a week do you want to workout?"] + 2.1 [label="Do you want to workout on back to back days?"] + 1 -> 1.1 [label=1] + condition1 [label=condition1 shape=box] + 1 -> condition1 [label=2] + 1.1 -> 2 [label=A] + 1.1 -> 2 [label=B] + 1.1 -> 2 [label=C] + 1.1 -> 2 [label=D] + 1.1 -> 2 [label=E] + 2 -> 2.1 [label=1] + condition3 [label=condition3 shape=box] + 2 -> condition3 [label=2] + condition3 [label=condition3 shape=box] + 2.1 -> condition3 [label=1] + condition3 [label=condition3 shape=box] + 2.1 -> condition3 [label=2] +} diff --git a/questionnaire_tree.png b/questionnaire_tree.png new file mode 100644 index 00000000..adc74048 Binary files /dev/null and b/questionnaire_tree.png differ diff --git a/website/.DS_Store b/website/.DS_Store new file mode 100644 index 00000000..aceda707 Binary files /dev/null and b/website/.DS_Store differ diff --git a/website/__init__.py b/website/__init__.py index 8d24a3dd..d64b4a2e 100644 --- a/website/__init__.py +++ b/website/__init__.py @@ -2,40 +2,60 @@ from flask_sqlalchemy import SQLAlchemy from os import path from flask_login import LoginManager +from flask_migrate import Migrate +# Create SQLAlchemy instance db = SQLAlchemy() DB_NAME = "database.db" +migrate = Migrate() def create_app(): app = Flask(__name__) app.config['SECRET_KEY'] = 'hjshjhdjah kjshkjdhjs' app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_NAME}' + + # Initialize SQLAlchemy with the Flask app db.init_app(app) + migrate.init_app(app, db) + # Import blueprints from .views import views from .auth import auth + from .weight_tracker import weight_tracker_bp # Import the weight tracker blueprint + # Register blueprints app.register_blueprint(views, url_prefix='/') - app.register_blueprint(auth, url_prefix='/') + app.register_blueprint(auth, url_prefix='/auth') + app.register_blueprint(weight_tracker_bp, url_prefix='/weight') # Register the weight tracker blueprint - from .models import User, Note + # Import models + from .models import User, Note, QuestionnaireResponse, WeightEntry with app.app_context(): + # Create database tables if they don't exist db.create_all() + # Initialize LoginManager login_manager = LoginManager() login_manager.login_view = 'auth.login' login_manager.init_app(app) + # Define the user loader function @login_manager.user_loader def load_user(id): return User.query.get(int(id)) + + # Inject current_user into templates + @app.context_processor + def inject_user(): + from flask_login import current_user + return dict(user=current_user) return app - def create_database(app): if not path.exists('website/' + DB_NAME): db.create_all(app=app) print('Created Database!') + diff --git a/website/auth.py b/website/auth.py index 8c65752e..4684b35d 100644 --- a/website/auth.py +++ b/website/auth.py @@ -1,13 +1,11 @@ -from flask import Blueprint, render_template, request, flash, redirect, url_for +from flask import Blueprint, render_template, redirect, url_for, request, flash from .models import User from werkzeug.security import generate_password_hash, check_password_hash -from . import db ##means from __init__.py import db from flask_login import login_user, login_required, logout_user, current_user - +from . import db auth = Blueprint('auth', __name__) - @auth.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': @@ -17,16 +15,15 @@ def login(): user = User.query.filter_by(email=email).first() if user: if check_password_hash(user.password, password): - flash('Logged in successfully!', category='success') login_user(user, remember=True) + if not user.has_completed_questionnaire: + return redirect(url_for('views.questionnaire')) return redirect(url_for('views.home')) else: - flash('Incorrect password, try again.', category='error') + flash('Incorrect password') else: - flash('Email does not exist.', category='error') - - return render_template("login.html", user=current_user) - + flash('Email does not exist') + return render_template('login.html') @auth.route('/logout') @login_required @@ -34,33 +31,29 @@ def logout(): logout_user() return redirect(url_for('auth.login')) - @auth.route('/sign-up', methods=['GET', 'POST']) def sign_up(): if request.method == 'POST': email = request.form.get('email') - first_name = request.form.get('firstName') + first_name = request.form.get('first_name') password1 = request.form.get('password1') password2 = request.form.get('password2') user = User.query.filter_by(email=email).first() if user: - flash('Email already exists.', category='error') - elif len(email) < 4: - flash('Email must be greater than 3 characters.', category='error') - elif len(first_name) < 2: - flash('First name must be greater than 1 character.', category='error') - elif password1 != password2: - flash('Passwords don\'t match.', category='error') - elif len(password1) < 7: - flash('Password must be at least 7 characters.', category='error') - else: - new_user = User(email=email, first_name=first_name, password=generate_password_hash( - password1, method='sha256')) - db.session.add(new_user) - db.session.commit() - login_user(new_user, remember=True) - flash('Account created!', category='success') - return redirect(url_for('views.home')) + flash('Email address already exists') + return redirect(url_for('auth.sign_up')) + + if password1 != password2: + flash('Passwords do not match') + return redirect(url_for('auth.sign_up')) + + new_user = User(email=email, first_name=first_name, password=generate_password_hash(password1, method='pbkdf2:sha256', salt_length=8)) + + db.session.add(new_user) + db.session.commit() + login_user(new_user, remember=True) + + return redirect(url_for('views.questionnaire')) - return render_template("sign_up.html", user=current_user) + return render_template('sign_up.html') diff --git a/website/forms.py b/website/forms.py new file mode 100644 index 00000000..aa97947a --- /dev/null +++ b/website/forms.py @@ -0,0 +1,17 @@ +from flask_wtf import FlaskForm +from wtforms import RadioField, SelectMultipleField, SubmitField +from wtforms.widgets import ListWidget, CheckboxInput + +class InitialQuestionForm(FlaskForm): + days = RadioField('How many days a week do you want to workout?', choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7')], coerce=int) + submit = SubmitField('Next') + +def create_dynamic_form(question): + class DynamicForm(FlaskForm): + if question.get('multi'): + answer = SelectMultipleField(question['text'], choices=question['choices'], option_widget=CheckboxInput(), widget=ListWidget(prefix_label=False)) + else: + answer = RadioField(question['text'], choices=question['choices'], coerce=str) + submit = SubmitField('Next') + return DynamicForm + diff --git a/website/models.py b/website/models.py index 79c17e6f..999f011b 100644 --- a/website/models.py +++ b/website/models.py @@ -1,18 +1,45 @@ -from . import db from flask_login import UserMixin -from sqlalchemy.sql import func +from . import db + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(150), unique=True) + password = db.Column(db.String(150)) + first_name = db.Column(db.String(150)) + has_completed_questionnaire = db.Column(db.Boolean, default=False) + +class Weight(db.Model): + id = db.Column(db.Integer, primary_key=True) + weight = db.Column(db.Float, nullable=False) + date = db.Column(db.DateTime, default=db.func.current_timestamp()) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', back_populates='weights') +User.weights = db.relationship('Weight', order_by=Weight.date, back_populates='user') + +class WeightEntry(db.Model): + id = db.Column(db.Integer, primary_key=True) + weight = db.Column(db.Float, nullable=False) + date = db.Column(db.DateTime, default=db.func.current_timestamp()) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', back_populates='weight_entries') + +User.weight_entries = db.relationship('WeightEntry', order_by=WeightEntry.date, back_populates='user') class Note(db.Model): id = db.Column(db.Integer, primary_key=True) data = db.Column(db.String(10000)) - date = db.Column(db.DateTime(timezone=True), default=func.now()) + date = db.Column(db.DateTime, default=db.func.current_timestamp()) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', back_populates='notes') +User.notes = db.relationship('Note', order_by=Note.date, back_populates='user') -class User(db.Model, UserMixin): +class QuestionnaireResponse(db.Model): id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(150), unique=True) - password = db.Column(db.String(150)) - first_name = db.Column(db.String(150)) - notes = db.relationship('Note') + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + question_id = db.Column(db.String(50)) + response = db.Column(db.String(1000)) + user = db.relationship('User', back_populates='questionnaire_responses') + +User.questionnaire_responses = db.relationship('QuestionnaireResponse', order_by=QuestionnaireResponse.id, back_populates='user') diff --git a/website/questionnaire.py b/website/questionnaire.py new file mode 100644 index 00000000..3b44a7cc --- /dev/null +++ b/website/questionnaire.py @@ -0,0 +1,113 @@ +from flask import session +from graphviz import Digraph +from .forms import InitialQuestionForm, create_dynamic_form + +questions_data = { + '1': { + 'text': 'Are there any muscle groups you DO NOT want to exercise?', + 'choices': [('1', 'Yes'), ('2', 'No')], + 'next': { + '1': '1.1', + '2': 'end_condition1' + } + }, + '1.1': { + 'text': 'Select all muscle groups you DO NOT want to exercise', + 'choices': [('A', 'Chest'), ('B', 'Back'), ('C', 'Arms'), ('D', 'Shoulders'), ('E', 'Legs')], + 'multi': True, + 'next': { + 'A': '2', + 'B': '2', + 'C': '2', + 'D': '2', + 'E': '2' + } + }, + '2': { + 'text': 'How many days a week do you want to workout?', + 'choices': [('1', '1 day'), ('2', '2 days'), ('3', '3 days'), ('4', '4 days'), ('5', '5 days'), ('6', '6 days'), ('7', '7 days')], + 'next': { + '1': '2.1', + '2': 'end_condition3' + } + }, + '2.1': { + 'text': 'Do you want to workout on back to back days?', + 'choices': [('1', 'Yes'), ('2', 'No')], + 'next': { + '1': 'end_condition3', + '2': 'end_condition3' + } + } +} + +conditions_messages = { + 'condition1': 'Message for condition 1', + 'condition2': 'Message for condition 2', + 'condition3': 'Message for condition 3' +} + +def get_next_question_and_update_session(answer, question): + next_question_id = None + if question.get('multi'): + answer = answer.split(',') + else: + answer = [answer] + session['responses'][session['current_question']] = answer + + for ans in answer: + next_id = question['next'].get(ans) + if next_id and next_id.startswith('end_condition'): + session['condition'] = next_id.replace('end_', '') + return None # End condition reached + elif next_id: + next_question_id = next_id + + if next_question_id: + session['history'].append(session['current_question']) + session['current_question'] = next_question_id + + return next_question_id + +def initialize_questionnaire(form): + selected_option = form.days.data + if selected_option: + session['days'] = selected_option + session['current_question'] = '1' # Assuming the first question is '1' + session['history'] = [] + session['responses'] = {} + return True + return False + +def get_question_and_form(): + current_question_id = session.get('current_question') + if current_question_id is None: + return None, None + + question = questions_data.get(current_question_id) + if question is None: + return None, None + + DynamicForm = create_dynamic_form(question) + form = DynamicForm() + return question, form + +def generate_question_tree(output_path='questionnaire_tree'): + dot = Digraph(comment='Questionnaire Tree') + + # Add nodes for each question + for question_id, question_info in questions_data.items(): + dot.node(question_id, question_info['text']) + + # Add edges for each choice + for question_id, question_info in questions_data.items(): + for choice, next_question_id in question_info['next'].items(): + if next_question_id.startswith('end_condition'): + next_question_id = next_question_id.replace('end_', '') + dot.node(next_question_id, next_question_id, shape='box') + dot.edge(question_id, next_question_id, label=choice) + + dot.render(output_path, format='png', view=False) + +if __name__ == '__main__': + generate_question_tree() diff --git a/website/static/newlogo.png b/website/static/newlogo.png new file mode 100644 index 00000000..75c65a34 Binary files /dev/null and b/website/static/newlogo.png differ diff --git a/website/static/style.css b/website/static/style.css new file mode 100644 index 00000000..f85b56ee --- /dev/null +++ b/website/static/style.css @@ -0,0 +1,164 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + font-family: 'Poppins', sans-serif; + box-sizing: border-box; +} + +body { + background: #fff; + color: #080808; +} + +/* Navigation bar styles */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 40px; /* Adjust padding for better spacing */ + background-color: #fff; /* White background */ + border-bottom: 1px solid #ddd; +} + +.navbar .logo { + display: flex; + align-items: center; + text-decoration: none; +} + +.navbar .logo-img { + height: 60px; /* Adjust as needed */ + width: auto; /* Maintain aspect ratio */ +} + +.navbar .nav-links { + display: flex; + list-style: none; +} + +.navbar .nav-links li { + margin-left: 30px; /* Adjust for better spacing */ +} + +.navbar .nav-links a { + text-decoration: none; + color: #080808; + font-size: 18px; + font-weight: bold; /* Make text bolder */ + position: relative; +} + +.navbar .nav-links a::after { + content: ''; + display: block; + width: 0; + height: 3px; /* Make underline bolder */ + background: #a317be; /* Purple color */ + transition: width .3s; + position: absolute; + bottom: -8px; + left: 0; +} + +.navbar .nav-links a:hover::after { + width: 100%; +} + +/* Notification styles */ +.dropdown-notification { + background-color: #f3e5f5; /* Light purple */ + color: #800080; /* Darker purple for text */ + border: none; + border-radius: 0; + padding: 10px 20px; + position: absolute; + top: 60px; + right: 20px; + display: none; + z-index: 1000; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.dropdown-notification.show { + display: block; +} + +/* Container for main content */ +.container { + max-width: 1200px; + margin: 20px auto; + padding: 20px; +} +/* style.css */ +/* Button styles */ +/* style.css */ +/* Button styles */ +/* style.css */ +/* Button styles */ +.btn { + background-color: white; /* White color by default */ + color: black; /* Black text color by default */ + border: 1px solid #a317be; /* Border color to distinguish buttons */ + padding: 10px 20px; + cursor: pointer; + border-radius: 5px; + font-size: 16px; + transition: background-color 0.3s ease, color 0.3s ease; + display: block; /* Make buttons block elements for vertical alignment */ + width: auto; /* Auto width to fit content */ + text-align: left; /* Align text to the left */ + margin-bottom: 10px; /* Space between buttons */ +} + +.btn:hover { + background-color: #a317be; /* Purple color on hover */ + color: white; /* White text color on hover */ +} + +.questionnaire-btn { + margin: 5px 0; /* Space between buttons */ + background-color: white; /* White by default */ + color: black; /* Black text by default */ +} + +.questionnaire-btn:hover { + background-color: #a317be; /* Purple color on hover */ + color: white; /* White text on hover */ +} +/* Dashboard styles */ +.dashboard-container { + display: flex; + justify-content: space-between; + gap: 20px; +} + +.dashboard-item { + flex: 1; + padding: 20px; + border: 1px solid #ddd; + border-radius: 10px; + background-color: #f9f9f9; + cursor: pointer; + transition: transform 0.3s, box-shadow 0.3s; + position: relative; +} + +.dashboard-item:hover { + transform: scale(1.05); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.dashboard-item h2 { + text-align: center; + margin-bottom: 20px; +} + +.dashboard-item canvas { + width: 100%; + height: 200px; +} + +.dashboard-item p { + text-align: center; +} diff --git a/website/templates/.DS_Store b/website/templates/.DS_Store new file mode 100644 index 00000000..984661d5 Binary files /dev/null and b/website/templates/.DS_Store differ diff --git a/website/templates/about.html b/website/templates/about.html new file mode 100644 index 00000000..2f63a683 --- /dev/null +++ b/website/templates/about.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block title %}About{% endblock %} +{% block content %} +

About PhySeek

+

Welcome to PhySeek, your personalized fitness destination!

+ +{% endblock %} diff --git a/website/templates/base.html b/website/templates/base.html index fbbd3854..fc00d0d8 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -1,91 +1,104 @@ - - - - - - + + + + + {% block title %}{% endblock %} + + + + + + + - {% with messages = get_flashed_messages(with_categories=true) %} {% if - messages %} {% for category, message in messages %} {% if category == - 'error' %} -