Compare commits
3 Commits
acad2def92
...
de66dc5fd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de66dc5fd8 | ||
|
|
ab23bf6a9e | ||
|
|
7b36a6795d |
@@ -33,7 +33,7 @@ def create_app():
|
|||||||
# Set up the user_loader function
|
# Set up the user_loader function
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(user_id)) # Query the User model by ID
|
return db.session.get(User, int(user_id))
|
||||||
|
|
||||||
# Register blueprints
|
# Register blueprints
|
||||||
from app.routes.auth import auth
|
from app.routes.auth import auth
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Base configuration."""
|
"""Base configuration."""
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY', '234234sdfsdfsdfsdf345345')
|
SECRET_KEY = os.environ.get('SECRET_KEY', '234234sdfsdfsdfsdf345345')
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
|
# Session and Remember Me configurations to keep user logged in almost indefinitely (10 years)
|
||||||
|
PERMANENT_SESSION_LIFETIME = timedelta(days=3650)
|
||||||
|
REMEMBER_COOKIE_DURATION = timedelta(days=3650)
|
||||||
|
|
||||||
class DevelopmentConfig(Config):
|
class DevelopmentConfig(Config):
|
||||||
"""Development configuration."""
|
"""Development configuration."""
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class User(UserMixin, db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(255), nullable=False, unique=True)
|
username = db.Column(db.String(255), nullable=False, unique=True)
|
||||||
password_hash = db.Column(db.Text, nullable=False)
|
password_hash = db.Column(db.Text, nullable=False)
|
||||||
profile = db.relationship('Profile', backref='user', uselist=False)
|
profile = db.relationship('Profile', backref='user', uselist=False, lazy='joined')
|
||||||
|
|
||||||
class Profile(db.Model):
|
class Profile(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -19,8 +19,13 @@ class Profile(db.Model):
|
|||||||
diastolic_threshold = db.Column(db.Integer, default=90)
|
diastolic_threshold = db.Column(db.Integer, default=90)
|
||||||
dark_mode = db.Column(db.Boolean, default=False)
|
dark_mode = db.Column(db.Boolean, default=False)
|
||||||
timezone = db.Column(db.String(50), default='UTC') # e.g., 'Australia/Sydney'
|
timezone = db.Column(db.String(50), default='UTC') # e.g., 'Australia/Sydney'
|
||||||
|
updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())
|
||||||
|
|
||||||
class Reading(db.Model):
|
class Reading(db.Model):
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('ix_reading_user_timestamp', 'user_id', 'timestamp'),
|
||||||
|
)
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
timestamp = db.Column(db.DateTime, nullable=False)
|
timestamp = db.Column(db.DateTime, nullable=False)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from app.models import db, User
|
from app.models import db, User, Profile
|
||||||
from app.forms import LoginForm, SignupForm
|
from app.forms import LoginForm, SignupForm
|
||||||
from flask_login import login_user, login_required, logout_user
|
from flask_login import login_user, login_required, logout_user
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ def signup():
|
|||||||
form = SignupForm()
|
form = SignupForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
hashed_password = generate_password_hash(form.password.data)
|
hashed_password = generate_password_hash(form.password.data)
|
||||||
new_user = User(username=form.username.data, password_hash=hashed_password)
|
new_profile = Profile()
|
||||||
|
new_user = User(username=form.username.data, password_hash=hashed_password, profile=new_profile)
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Account created successfully. Please log in.", "success")
|
flash("Account created successfully. Please log in.", "success")
|
||||||
@@ -24,7 +25,9 @@ def login():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(username=form.username.data).first()
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
if user and check_password_hash(user.password_hash, form.password.data):
|
if user and check_password_hash(user.password_hash, form.password.data):
|
||||||
login_user(user)
|
from flask import session
|
||||||
|
session.permanent = True
|
||||||
|
login_user(user, remember=True)
|
||||||
flash("Logged in successfully.", "success")
|
flash("Logged in successfully.", "success")
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
flash("Invalid username or password.", "danger")
|
flash("Invalid username or password.", "danger")
|
||||||
|
|||||||
@@ -41,15 +41,23 @@ def manage_data():
|
|||||||
@login_required
|
@login_required
|
||||||
def export_data():
|
def export_data():
|
||||||
import io
|
import io
|
||||||
|
from flask import Response
|
||||||
|
|
||||||
|
def generate_csv():
|
||||||
|
"""Stream CSV rows to avoid loading all readings into memory."""
|
||||||
|
# Write header
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output)
|
writer = csv.writer(output)
|
||||||
|
|
||||||
# Write CSV header
|
|
||||||
writer.writerow(['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate'])
|
writer.writerow(['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate'])
|
||||||
|
yield output.getvalue()
|
||||||
|
output.seek(0)
|
||||||
|
output.truncate(0)
|
||||||
|
|
||||||
|
# Stream readings in chunks using yield_per
|
||||||
|
readings = Reading.query.filter_by(user_id=current_user.id).order_by(
|
||||||
|
Reading.timestamp
|
||||||
|
).yield_per(500)
|
||||||
|
|
||||||
# Write user readings to the CSV
|
|
||||||
readings = Reading.query.filter_by(user_id=current_user.id).all()
|
|
||||||
for reading in readings:
|
for reading in readings:
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
reading.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
reading.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
@@ -57,15 +65,12 @@ def export_data():
|
|||||||
reading.diastolic,
|
reading.diastolic,
|
||||||
reading.heart_rate,
|
reading.heart_rate,
|
||||||
])
|
])
|
||||||
|
yield output.getvalue()
|
||||||
# Convert text to bytes for `send_file`
|
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
response = io.BytesIO(output.getvalue().encode('utf-8'))
|
output.truncate(0)
|
||||||
output.close()
|
|
||||||
|
|
||||||
return send_file(
|
return Response(
|
||||||
response,
|
generate_csv(),
|
||||||
mimetype='text/csv',
|
mimetype='text/csv',
|
||||||
as_attachment=True,
|
headers={'Content-Disposition': 'attachment; filename=readings.csv'}
|
||||||
download_name='readings.csv'
|
|
||||||
)
|
)
|
||||||
@@ -10,6 +10,9 @@ from datetime import date, datetime, timedelta
|
|||||||
|
|
||||||
main = Blueprint('main', __name__)
|
main = Blueprint('main', __name__)
|
||||||
|
|
||||||
|
# Number of readings to show per page in list view
|
||||||
|
PAGE_SIZE = 50
|
||||||
|
|
||||||
@main.route('/', methods=['GET'])
|
@main.route('/', methods=['GET'])
|
||||||
def landing():
|
def landing():
|
||||||
return redirect(url_for('main.dashboard')) if current_user.is_authenticated else render_template('landing.html')
|
return redirect(url_for('main.dashboard')) if current_user.is_authenticated else render_template('landing.html')
|
||||||
@@ -29,26 +32,42 @@ def dashboard():
|
|||||||
start_date = request.form.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d'))
|
start_date = request.form.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d'))
|
||||||
end_date = request.form.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d'))
|
end_date = request.form.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
# Fetch filtered readings
|
# Pagination for list view
|
||||||
readings = fetch_readings(current_user.id, start_date, end_date, user_tz)
|
page = request.args.get('page', 1, type=int)
|
||||||
|
|
||||||
# Annotate readings with relative and localized timestamps
|
# Fetch paginated readings for the list view
|
||||||
annotate_readings(readings, user_tz)
|
paginated = fetch_readings_paginated(current_user.id, start_date, end_date, user_tz, page, PAGE_SIZE)
|
||||||
|
|
||||||
# Generate calendar view
|
# For calendar/graph/badges, fetch only current month + week readings (much smaller set)
|
||||||
week_view = generate_weekly_calendar(readings, datetime.now(user_tz), user_tz)
|
now = datetime.now(user_tz)
|
||||||
month_view = generate_monthly_calendar(readings, datetime.now(user_tz), user_tz)
|
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
month_start_utc = month_start.astimezone(utc)
|
||||||
|
calendar_readings = fetch_readings_for_range(current_user.id, month_start_utc)
|
||||||
|
|
||||||
# Calculate stats and badges
|
# Annotate all readings with relative and localized timestamps
|
||||||
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary(readings)
|
annotate_readings(paginated.items, user_tz)
|
||||||
badges = calculate_progress_badges(readings)
|
annotate_readings(calendar_readings, user_tz)
|
||||||
|
|
||||||
# Prepare graph data
|
# Build shared lookup for calendar views (single pass)
|
||||||
graph_data = prepare_graph_data(readings)
|
readings_by_day = build_readings_by_day(calendar_readings, user_tz)
|
||||||
|
|
||||||
|
# Generate calendar views from the shared lookup
|
||||||
|
week_view = generate_weekly_calendar(readings_by_day, now, user_tz)
|
||||||
|
month_view = generate_monthly_calendar(readings_by_day, now, user_tz)
|
||||||
|
|
||||||
|
# Calculate weekly averages via SQL (much faster than Python)
|
||||||
|
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary_sql(current_user.id)
|
||||||
|
|
||||||
|
# Badges from the paginated readings (or full set if needed)
|
||||||
|
badges = calculate_progress_badges(paginated.items)
|
||||||
|
|
||||||
|
# Prepare graph data from calendar readings (current month)
|
||||||
|
graph_data = prepare_graph_data(calendar_readings)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'dashboard.html',
|
'dashboard.html',
|
||||||
readings=readings,
|
readings=paginated.items,
|
||||||
|
pagination=paginated,
|
||||||
profile=current_user.profile,
|
profile=current_user.profile,
|
||||||
badges=badges,
|
badges=badges,
|
||||||
systolic_avg=systolic_avg,
|
systolic_avg=systolic_avg,
|
||||||
@@ -58,7 +77,7 @@ def dashboard():
|
|||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
month=month_view,
|
month=month_view,
|
||||||
week = week_view,
|
week=week_view,
|
||||||
date=date,
|
date=date,
|
||||||
**graph_data
|
**graph_data
|
||||||
)
|
)
|
||||||
@@ -75,22 +94,32 @@ def get_reading_date_range(user_id, user_tz):
|
|||||||
return first, last
|
return first, last
|
||||||
|
|
||||||
|
|
||||||
def fetch_readings(user_id, start_date, end_date, user_tz):
|
def fetch_readings_paginated(user_id, start_date, end_date, user_tz, page, per_page):
|
||||||
"""Retrieve readings filtered by date range."""
|
"""Retrieve paginated readings filtered by date range."""
|
||||||
query = Reading.query.filter_by(user_id=user_id)
|
query = Reading.query.filter_by(user_id=user_id)
|
||||||
|
|
||||||
if start_date and end_date:
|
if start_date and end_date:
|
||||||
# Convert dates to the user's timezone
|
|
||||||
start_dt = user_tz.localize(datetime.strptime(start_date, '%Y-%m-%d')).astimezone(utc)
|
start_dt = user_tz.localize(datetime.strptime(start_date, '%Y-%m-%d')).astimezone(utc)
|
||||||
end_dt = user_tz.localize(datetime.strptime(end_date, '%Y-%m-%d')).astimezone(utc) + timedelta(days=1) - timedelta(seconds=1)
|
end_dt = user_tz.localize(datetime.strptime(end_date, '%Y-%m-%d')).astimezone(utc) + timedelta(days=1) - timedelta(seconds=1)
|
||||||
|
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
Reading.timestamp >= start_dt,
|
Reading.timestamp >= start_dt,
|
||||||
Reading.timestamp <= end_dt
|
Reading.timestamp <= end_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return query.order_by(Reading.timestamp.desc()).paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_readings_for_range(user_id, start_utc, end_utc=None):
|
||||||
|
"""Fetch readings from a UTC start time onwards (for calendar/graph views)."""
|
||||||
|
query = Reading.query.filter(
|
||||||
|
Reading.user_id == user_id,
|
||||||
|
Reading.timestamp >= start_utc
|
||||||
|
)
|
||||||
|
if end_utc:
|
||||||
|
query = query.filter(Reading.timestamp <= end_utc)
|
||||||
return query.order_by(Reading.timestamp.desc()).all()
|
return query.order_by(Reading.timestamp.desc()).all()
|
||||||
|
|
||||||
|
|
||||||
def annotate_readings(readings, user_tz):
|
def annotate_readings(readings, user_tz):
|
||||||
"""Add relative and localized timestamps to readings."""
|
"""Add relative and localized timestamps to readings."""
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
@@ -98,30 +127,40 @@ def annotate_readings(readings, user_tz):
|
|||||||
reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp)
|
reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp)
|
||||||
reading.local_timestamp = utc.localize(reading.timestamp).astimezone(user_tz)
|
reading.local_timestamp = utc.localize(reading.timestamp).astimezone(user_tz)
|
||||||
|
|
||||||
def calculate_weekly_summary(readings):
|
|
||||||
"""Calculate averages for the past week."""
|
def build_readings_by_day(readings, user_tz):
|
||||||
|
"""Build a dict mapping dates to readings (single pass, shared by calendar views)."""
|
||||||
|
readings_by_day = defaultdict(list)
|
||||||
|
for reading in readings:
|
||||||
|
local_date = reading.local_timestamp.date() if hasattr(reading, 'local_timestamp') else utc.localize(reading.timestamp).astimezone(user_tz).date()
|
||||||
|
readings_by_day[local_date].append(reading)
|
||||||
|
return readings_by_day
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_weekly_summary_sql(user_id):
|
||||||
|
"""Calculate weekly averages using SQL aggregation (single DB query)."""
|
||||||
one_week_ago = datetime.utcnow() - timedelta(days=7)
|
one_week_ago = datetime.utcnow() - timedelta(days=7)
|
||||||
weekly_readings = [r for r in readings if r.timestamp >= one_week_ago]
|
result = db.session.query(
|
||||||
if weekly_readings:
|
func.round(func.avg(Reading.systolic), 1).label('sys_avg'),
|
||||||
return (
|
func.round(func.avg(Reading.diastolic), 1).label('dia_avg'),
|
||||||
round(sum(r.systolic for r in weekly_readings) / len(weekly_readings), 1),
|
func.round(func.avg(Reading.heart_rate), 1).label('hr_avg'),
|
||||||
round(sum(r.diastolic for r in weekly_readings) / len(weekly_readings), 1),
|
).filter(
|
||||||
round(sum(r.heart_rate for r in weekly_readings) / len(weekly_readings), 1),
|
Reading.user_id == user_id,
|
||||||
)
|
Reading.timestamp >= one_week_ago
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if result and result.sys_avg is not None:
|
||||||
|
return float(result.sys_avg), float(result.dia_avg), float(result.hr_avg)
|
||||||
return 0, 0, 0
|
return 0, 0, 0
|
||||||
|
|
||||||
def generate_monthly_calendar(readings, selected_date, local_tz):
|
|
||||||
"""Generate a monthly calendar view."""
|
def generate_monthly_calendar(readings_by_day, selected_date, local_tz):
|
||||||
|
"""Generate a monthly calendar view from pre-built readings_by_day."""
|
||||||
today = datetime.now(local_tz).date()
|
today = datetime.now(local_tz).date()
|
||||||
first_day = selected_date.replace(day=1)
|
first_day = selected_date.replace(day=1)
|
||||||
start_date = first_day - timedelta(days=(first_day.weekday() + 1) % 7)
|
start_date = first_day - timedelta(days=(first_day.weekday() + 1) % 7)
|
||||||
end_date = start_date + timedelta(days=41)
|
end_date = start_date + timedelta(days=41)
|
||||||
|
|
||||||
readings_by_day = defaultdict(list)
|
|
||||||
for reading in readings:
|
|
||||||
local_date = reading.timestamp.astimezone(local_tz).date()
|
|
||||||
readings_by_day[local_date].append(reading)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'day': current_date.day,
|
'day': current_date.day,
|
||||||
@@ -132,23 +171,14 @@ def generate_monthly_calendar(readings, selected_date, local_tz):
|
|||||||
for current_date in (start_date + timedelta(days=i) for i in range((end_date - start_date).days + 1))
|
for current_date in (start_date + timedelta(days=i) for i in range((end_date - start_date).days + 1))
|
||||||
]
|
]
|
||||||
|
|
||||||
def generate_weekly_calendar(readings, selected_date, local_tz):
|
def generate_weekly_calendar(readings_by_day, selected_date, local_tz):
|
||||||
"""Generate a weekly calendar view."""
|
"""Generate a weekly calendar view from pre-built readings_by_day."""
|
||||||
# Get the start of the week (Monday) and the end of the week (Sunday)
|
|
||||||
today = datetime.now(local_tz).date()
|
today = datetime.now(local_tz).date()
|
||||||
start_of_week = selected_date - timedelta(days=selected_date.weekday())
|
start_of_week = selected_date - timedelta(days=selected_date.weekday())
|
||||||
end_of_week = start_of_week + timedelta(days=6)
|
|
||||||
|
|
||||||
# Group readings by day
|
|
||||||
readings_by_day = defaultdict(list)
|
|
||||||
for reading in readings:
|
|
||||||
local_date = reading.timestamp.astimezone(local_tz).date()
|
|
||||||
readings_by_day[local_date].append(reading)
|
|
||||||
|
|
||||||
# Build the weekly calendar view
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'date': current_date.strftime('%a, %b %d') ,
|
'date': current_date.strftime('%a, %b %d'),
|
||||||
'is_today': current_date == today,
|
'is_today': current_date == today,
|
||||||
'readings': readings_by_day.get(current_date.date(), []),
|
'readings': readings_by_day.get(current_date.date(), []),
|
||||||
}
|
}
|
||||||
@@ -167,22 +197,21 @@ def prepare_graph_data(readings):
|
|||||||
def calculate_progress_badges(readings):
|
def calculate_progress_badges(readings):
|
||||||
"""Generate badges based on user activity and milestones."""
|
"""Generate badges based on user activity and milestones."""
|
||||||
now = datetime.utcnow().date()
|
now = datetime.utcnow().date()
|
||||||
streak_count, badges = 1, []
|
badges = []
|
||||||
|
|
||||||
if not readings:
|
if not readings:
|
||||||
return badges
|
return badges
|
||||||
|
|
||||||
sorted_readings = sorted(readings, key=lambda r: r.timestamp.date())
|
# Use reversed() instead of re-sorting — readings come in desc order from DB
|
||||||
|
sorted_readings = list(reversed(readings))
|
||||||
streak_count = 1
|
streak_count = 1
|
||||||
daily_streak = True
|
daily_streak = True
|
||||||
|
|
||||||
# Start with the first reading
|
|
||||||
previous_date = sorted_readings[0].timestamp.date()
|
previous_date = sorted_readings[0].timestamp.date()
|
||||||
|
|
||||||
for reading in sorted_readings[1:]:
|
for reading in sorted_readings[1:]:
|
||||||
current_date = reading.timestamp.date()
|
current_date = reading.timestamp.date()
|
||||||
|
|
||||||
# Check for consecutive daily streaks
|
|
||||||
if (current_date - previous_date).days == 1:
|
if (current_date - previous_date).days == 1:
|
||||||
streak_count += 1
|
streak_count += 1
|
||||||
elif (current_date - previous_date).days > 1:
|
elif (current_date - previous_date).days > 1:
|
||||||
@@ -190,7 +219,6 @@ def calculate_progress_badges(readings):
|
|||||||
|
|
||||||
previous_date = current_date
|
previous_date = current_date
|
||||||
|
|
||||||
# Add streak badges
|
|
||||||
if daily_streak and streak_count >= 1:
|
if daily_streak and streak_count >= 1:
|
||||||
badges.append(f"Current Streak: {streak_count} Days")
|
badges.append(f"Current Streak: {streak_count} Days")
|
||||||
if daily_streak and streak_count >= 7:
|
if daily_streak and streak_count >= 7:
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ def profile_image(user_id):
|
|||||||
response.headers.set('Content-Type', 'image/jpeg')
|
response.headers.set('Content-Type', 'image/jpeg')
|
||||||
response.headers.set('Cache-Control', 'public, max-age=86400') # Cache for 1 day
|
response.headers.set('Cache-Control', 'public, max-age=86400') # Cache for 1 day
|
||||||
response.headers.set('ETag', str(hash(profile.profile_pic))) # Unique ETag for the image
|
response.headers.set('ETag', str(hash(profile.profile_pic))) # Unique ETag for the image
|
||||||
response.headers.set('Last-Modified', http_date(datetime.utcnow().timestamp()))
|
# Use actual profile update time instead of utcnow() which defeats caching
|
||||||
|
last_modified = profile.updated_at or datetime.utcnow()
|
||||||
|
response.headers.set('Last-Modified', http_date(last_modified.timestamp()))
|
||||||
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ html,
|
|||||||
-o-tab-size: 4;
|
-o-tab-size: 4;
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
/* 3 */
|
/* 3 */
|
||||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
font-family: Inter, sans-serif;
|
||||||
/* 4 */
|
/* 4 */
|
||||||
font-feature-settings: normal;
|
font-feature-settings: normal;
|
||||||
/* 5 */
|
/* 5 */
|
||||||
@@ -588,6 +588,10 @@ video {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -608,6 +612,15 @@ video {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inset-0 {
|
||||||
|
inset: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inset-y-0 {
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-0 {
|
.bottom-0 {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
@@ -644,10 +657,22 @@ video {
|
|||||||
top: 1.25rem;
|
top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-24 {
|
||||||
|
top: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-0 {
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.z-10 {
|
.z-10 {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.z-50 {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
.col-span-full {
|
.col-span-full {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
@@ -657,6 +682,10 @@ video {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.-mb-px {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-2 {
|
.mb-2 {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -681,10 +710,6 @@ video {
|
|||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-2 {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mr-3 {
|
.mr-3 {
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -717,12 +742,36 @@ video {
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.-mb-px {
|
.ml-4 {
|
||||||
margin-bottom: -1px;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-0 {
|
.mb-12 {
|
||||||
margin-left: 0px;
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-16 {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-20 {
|
||||||
|
margin-bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-5 {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-12 {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-8 {
|
||||||
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
@@ -750,10 +799,18 @@ video {
|
|||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-10 {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-16 {
|
.h-16 {
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-36 {
|
||||||
|
height: 9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-4 {
|
.h-4 {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
@@ -774,12 +831,12 @@ video {
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-10 {
|
.h-20 {
|
||||||
height: 2.5rem;
|
height: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-36 {
|
.h-2 {
|
||||||
height: 9rem;
|
height: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-16 {
|
.w-16 {
|
||||||
@@ -810,6 +867,18 @@ video {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-10 {
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-20 {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-w-\[300px\] {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
.max-w-4xl {
|
.max-w-4xl {
|
||||||
max-width: 56rem;
|
max-width: 56rem;
|
||||||
}
|
}
|
||||||
@@ -826,6 +895,10 @@ video {
|
|||||||
max-width: 28rem;
|
max-width: 28rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-w-2xl {
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
}
|
}
|
||||||
@@ -862,10 +935,6 @@ video {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items-start {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -898,6 +967,10 @@ video {
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gap-10 {
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||||
@@ -932,12 +1005,6 @@ video {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.truncate {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -954,6 +1021,23 @@ video {
|
|||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-2xl {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-3xl {
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-b-3xl {
|
||||||
|
border-bottom-right-radius: 1.5rem;
|
||||||
|
border-bottom-left-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
@@ -979,19 +1063,39 @@ video {
|
|||||||
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1));
|
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-gray-200 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.border-gray-300 {
|
.border-gray-300 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-green-50 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(240 253 244 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.border-white {
|
.border-white {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
|
border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-green-50 {
|
.border-gray-100 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(240 253 244 / var(--tw-border-opacity, 1));
|
border-color: rgb(243 244 246 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-primary-500 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(20 184 166 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-primary-600 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(13 148 136 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-blue-600 {
|
.bg-blue-600 {
|
||||||
@@ -1009,6 +1113,11 @@ video {
|
|||||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-gray-200 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-gray-300 {
|
.bg-gray-300 {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
||||||
@@ -1049,20 +1158,88 @@ video {
|
|||||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-primary-800 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(17 94 89 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-900 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(19 78 74 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-50 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(240 253 250 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-700 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(15 118 110 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white\/5 {
|
||||||
|
background-color: rgb(255 255 255 / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-600 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(13 148 136 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-100 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(204 251 241 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-gradient-to-r {
|
.bg-gradient-to-r {
|
||||||
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-gradient-to-br {
|
||||||
|
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
|
||||||
|
}
|
||||||
|
|
||||||
.from-blue-500 {
|
.from-blue-500 {
|
||||||
--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);
|
--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);
|
||||||
--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);
|
--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);
|
||||||
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.from-primary-600 {
|
||||||
|
--tw-gradient-from: #0d9488 var(--tw-gradient-from-position);
|
||||||
|
--tw-gradient-to: rgb(13 148 136 / 0) var(--tw-gradient-to-position);
|
||||||
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-primary-400 {
|
||||||
|
--tw-gradient-from: #2dd4bf var(--tw-gradient-from-position);
|
||||||
|
--tw-gradient-to: rgb(45 212 191 / 0) var(--tw-gradient-to-position);
|
||||||
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-primary-500 {
|
||||||
|
--tw-gradient-from: #14b8a6 var(--tw-gradient-from-position);
|
||||||
|
--tw-gradient-to: rgb(20 184 166 / 0) var(--tw-gradient-to-position);
|
||||||
|
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
|
||||||
|
}
|
||||||
|
|
||||||
.to-blue-700 {
|
.to-blue-700 {
|
||||||
--tw-gradient-to: #1d4ed8 var(--tw-gradient-to-position);
|
--tw-gradient-to: #1d4ed8 var(--tw-gradient-to-position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.to-primary-800 {
|
||||||
|
--tw-gradient-to: #115e59 var(--tw-gradient-to-position);
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-primary-600 {
|
||||||
|
--tw-gradient-to: #0d9488 var(--tw-gradient-to-position);
|
||||||
|
}
|
||||||
|
|
||||||
|
.to-primary-700 {
|
||||||
|
--tw-gradient-to: #0f766e var(--tw-gradient-to-position);
|
||||||
|
}
|
||||||
|
|
||||||
.fill-current {
|
.fill-current {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
@@ -1100,11 +1277,20 @@ video {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-10 {
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.px-2 {
|
.px-2 {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.px-4 {
|
.px-4 {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
@@ -1120,6 +1306,11 @@ video {
|
|||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.py-1 {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.py-16 {
|
.py-16 {
|
||||||
padding-top: 4rem;
|
padding-top: 4rem;
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
@@ -1145,28 +1336,19 @@ video {
|
|||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.px-0 {
|
.py-24 {
|
||||||
padding-left: 0px;
|
padding-top: 6rem;
|
||||||
padding-right: 0px;
|
padding-bottom: 6rem;
|
||||||
}
|
|
||||||
|
|
||||||
.py-1 {
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pl-2 {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pt-6 {
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-1 {
|
.pl-1 {
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pl-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pr-2 {
|
.pr-2 {
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -1175,14 +1357,34 @@ video {
|
|||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-left {
|
.pt-6 {
|
||||||
text-align: left;
|
padding-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-20 {
|
||||||
|
padding-bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pt-24 {
|
||||||
|
padding-top: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-11 {
|
||||||
|
padding-left: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-sans {
|
||||||
|
font-family: Inter, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
.text-2xl {
|
.text-2xl {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
@@ -1223,6 +1425,11 @@ video {
|
|||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-5xl {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.font-bold {
|
.font-bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -1235,12 +1442,24 @@ video {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-light {
|
.font-extrabold {
|
||||||
font-weight: 300;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leading-none {
|
.uppercase {
|
||||||
line-height: 1;
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leading-relaxed {
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-tight {
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-wider {
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-blue-600 {
|
.text-blue-600 {
|
||||||
@@ -1298,10 +1517,40 @@ video {
|
|||||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-gray-900 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary-50 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(240 253 250 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(13 148 136 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary-700 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(15 118 110 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary-200 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(153 246 228 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.no-underline {
|
.no-underline {
|
||||||
text-decoration-line: none;
|
text-decoration-line: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.antialiased {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
.shadow {
|
.shadow {
|
||||||
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||||
@@ -1326,6 +1575,24 @@ video {
|
|||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shadow-xl {
|
||||||
|
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||||
|
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-inner {
|
||||||
|
--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
|
||||||
|
--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-2xl {
|
||||||
|
--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||||
|
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.transition {
|
.transition {
|
||||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
|
||||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||||
@@ -1340,6 +1607,37 @@ video {
|
|||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-all {
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-300 {
|
||||||
|
transition-duration: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:-translate-y-1:hover {
|
||||||
|
--tw-translate-y: -0.25rem;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:-translate-y-2:hover {
|
||||||
|
--tw-translate-y: -0.5rem;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:-translate-y-0\.5:hover {
|
||||||
|
--tw-translate-y: -0.125rem;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:bg-blue-700:hover {
|
.hover\:bg-blue-700:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
|
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
|
||||||
@@ -1355,6 +1653,11 @@ video {
|
|||||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
|
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:bg-gray-300:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:bg-gray-400:hover {
|
.hover\:bg-gray-400:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
|
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
|
||||||
@@ -1375,6 +1678,21 @@ video {
|
|||||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity, 1));
|
background-color: rgb(185 28 28 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:bg-gray-50:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-primary-600:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(13 148 136 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-primary-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(15 118 110 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:text-gray-200:hover {
|
.hover\:text-gray-200:hover {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(229 231 235 / var(--tw-text-opacity, 1));
|
color: rgb(229 231 235 / var(--tw-text-opacity, 1));
|
||||||
@@ -1405,6 +1723,11 @@ video {
|
|||||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:text-primary-800:hover {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(17 94 89 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:underline:hover {
|
.hover\:underline:hover {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
@@ -1419,11 +1742,33 @@ video {
|
|||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:shadow-2xl:hover {
|
||||||
|
--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||||
|
--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:shadow-xl:hover {
|
||||||
|
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||||
|
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px 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 {
|
.focus\:border-blue-500:focus {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focus\:border-primary-500:focus {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(20 184 166 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus\:bg-white:focus {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.focus\:text-white:focus {
|
.focus\:text-white:focus {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
@@ -1450,6 +1795,11 @@ video {
|
|||||||
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1));
|
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focus\:ring-primary-500:focus {
|
||||||
|
--tw-ring-opacity: 1;
|
||||||
|
--tw-ring-color: rgb(20 184 166 / var(--tw-ring-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.group:hover .group-hover\:scale-105 {
|
.group:hover .group-hover\:scale-105 {
|
||||||
--tw-scale-x: 1.05;
|
--tw-scale-x: 1.05;
|
||||||
--tw-scale-y: 1.05;
|
--tw-scale-y: 1.05;
|
||||||
@@ -1461,10 +1811,6 @@ video {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.sm\:ml-0\.5 {
|
|
||||||
margin-left: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:block {
|
.sm\:block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -1477,17 +1823,24 @@ video {
|
|||||||
height: 10rem;
|
height: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:px-0\.5 {
|
.sm\:flex-row {
|
||||||
padding-left: 0.125rem;
|
flex-direction: row;
|
||||||
padding-right: 0.125rem;
|
}
|
||||||
|
|
||||||
|
.sm\:space-x-6 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(1.5rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.md\:ml-2 {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.md\:block {
|
.md\:block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -1496,8 +1849,8 @@ video {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md\:w-auto {
|
.md\:w-1\/3 {
|
||||||
width: auto;
|
width: 33.333333%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md\:grid-cols-2 {
|
.md\:grid-cols-2 {
|
||||||
@@ -1516,17 +1869,18 @@ video {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md\:px-0\.5 {
|
.md\:text-2xl {
|
||||||
padding-left: 0.125rem;
|
font-size: 1.5rem;
|
||||||
padding-right: 0.125rem;
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md\:text-6xl {
|
||||||
|
font-size: 3.75rem;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.lg\:ml-2 {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg\:block {
|
.lg\:block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -1551,11 +1905,6 @@ video {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg\:px-0\.5 {
|
|
||||||
padding-left: 0.125rem;
|
|
||||||
padding-right: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg\:pt-0 {
|
.lg\:pt-0 {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
@@ -1569,8 +1918,4 @@ video {
|
|||||||
.xl\:hidden {
|
.xl\:hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xl\:flex-row {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
3
app/static/images/favicon.svg
Normal file
3
app/static/images/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#14b8a6">
|
||||||
|
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
@@ -5,17 +5,17 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}BP Tracker{% endblock %}</title>
|
<title>{% block title %}BP Tracker{% endblock %}</title>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='images/favicon-32x32.png') }}">
|
<link rel="icon" type="image/svg+xml" sizes="any" href="{{ url_for('static', filename='images/favicon.svg') }}">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='images/favicon-32x32.png') }}">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<link href="/static/css/tailwind.css" rel="stylesheet">
|
<link href="/static/css/tailwind.css" rel="stylesheet">
|
||||||
<script src="/static/js/alpine.min.js" defer></script>
|
<script src="/static/js/alpine.min.js" defer></script>
|
||||||
<script src="/static/js/turbolinks.min.js"></script>
|
<script src="/static/js/turbolinks.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-100 text-gray-800">
|
<body class="bg-gray-50 text-gray-800 font-sans antialiased">
|
||||||
<nav class="flex items-center justify-between flex-wrap p-6 fixed w-full z-10 top-0" x-data="{ isOpen: false }"
|
<nav class="flex items-center justify-between flex-wrap p-6 fixed w-full z-10 top-0 transition-colors duration-300 shadow-md"
|
||||||
@keydown.escape="isOpen = false" @click.away="isOpen = false"
|
x-data="{ isOpen: false }" @keydown.escape="isOpen = false" @click.away="isOpen = false"
|
||||||
:class="{ 'shadow-lg bg-indigo-900' : isOpen , 'bg-gray-800' : !isOpen}">
|
:class="{ 'bg-primary-900' : isOpen , 'bg-primary-800' : !isOpen}">
|
||||||
<!--Logo etc-->
|
<!--Logo etc-->
|
||||||
<div class="flex items-center flex-shrink-0 text-white mr-6">
|
<div class="flex items-center flex-shrink-0 text-white mr-6">
|
||||||
<a class="text-white no-underline hover:text-white hover:no-underline" href="/">
|
<a class="text-white no-underline hover:text-white hover:no-underline" href="/">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
{% if request.path == url_for('main.dashboard') %}
|
{% if request.path == url_for('main.dashboard') %}
|
||||||
text-white
|
text-white
|
||||||
{% else %}
|
{% else %}
|
||||||
text-gray-600 hover:text-gray-200 hover:text-underline
|
text-primary-200 hover:text-white font-medium transition-colors
|
||||||
{% endif %}" href="{{ url_for('main.dashboard') }}">Dashboard
|
{% endif %}" href="{{ url_for('main.dashboard') }}">Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -54,12 +54,12 @@
|
|||||||
{% if request.path == url_for('data.manage_data') %}
|
{% if request.path == url_for('data.manage_data') %}
|
||||||
text-white
|
text-white
|
||||||
{% else %}
|
{% else %}
|
||||||
text-gray-600 hover:text-gray-200 hover:text-underline
|
text-primary-200 hover:text-white font-medium transition-colors
|
||||||
{% endif %}" href="{{ url_for('data.manage_data') }}">Data
|
{% endif %}" href="{{ url_for('data.manage_data') }}">Data
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mr-3">
|
<li class="mr-3">
|
||||||
<a class="flex items-center gap-2 text-gray-600 no-underline hover:text-gray-200 hover:text-underline py-2 px-4"
|
<a class="flex items-center gap-2 text-primary-200 no-underline hover:text-white font-medium transition-colors py-2 px-4"
|
||||||
href="{{ url_for('user.profile') }}">
|
href="{{ url_for('user.profile') }}">
|
||||||
{% if current_user.profile and current_user.profile.profile_pic %}
|
{% if current_user.profile and current_user.profile.profile_pic %}
|
||||||
<img src="{{ url_for('user.profile_image', user_id=current_user.id) }}" alt="Profile Picture"
|
<img src="{{ url_for('user.profile_image', user_id=current_user.id) }}" alt="Profile Picture"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<!-- Default SVG Icon -->
|
<!-- Default SVG Icon -->
|
||||||
<div
|
<div
|
||||||
class="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center border-2 border-white">
|
class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center border-2 border-white">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-5 h-5 text-gray-600"
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="w-5 h-5 text-gray-600"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
{% if request.path == url_for('user.profile') %}
|
{% if request.path == url_for('user.profile') %}
|
||||||
text-white
|
text-white
|
||||||
{% else %}
|
{% else %}
|
||||||
text-gray-600 hover:text-gray-200 hover:text-underline
|
text-primary-200 hover:text-white font-medium transition-colors
|
||||||
{% endif %}">Profile</span>
|
{% endif %}">Profile</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
{% if request.path == url_for('auth.logout') %}
|
{% if request.path == url_for('auth.logout') %}
|
||||||
text-white
|
text-white
|
||||||
{% else %}
|
{% else %}
|
||||||
text-gray-600 hover:text-gray-200 hover:text-underline
|
text-primary-200 hover:text-white font-medium transition-colors
|
||||||
{% endif %}" href="{{ url_for('auth.logout') }}">Logout
|
{% endif %}" href="{{ url_for('auth.logout') }}">Logout
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
{% if request.path == url_for('auth.login') %}
|
{% if request.path == url_for('auth.login') %}
|
||||||
text-white
|
text-white
|
||||||
{% else %}
|
{% else %}
|
||||||
text-gray-600 hover:text-gray-200 hover:text-underline
|
text-primary-200 hover:text-white font-medium transition-colors
|
||||||
{% endif %}" href="{{ url_for('auth.login') }}">Login
|
{% endif %}" href="{{ url_for('auth.login') }}">Login
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
{% if request.path == url_for('auth.signup') %}
|
{% if request.path == url_for('auth.signup') %}
|
||||||
text-white
|
text-white
|
||||||
{% else %}
|
{% else %}
|
||||||
text-gray-600 hover:text-gray-200 hover:text-underline
|
text-primary-200 hover:text-white font-medium transition-colors
|
||||||
{% endif %}" href="{{ url_for('auth.signup') }}">Signup
|
{% endif %}" href="{{ url_for('auth.signup') }}">Signup
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -118,12 +118,13 @@
|
|||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="container mx-auto mt-4 space-y-4">
|
<div class="fixed top-24 right-4 z-50 space-y-4">
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="flex items-center justify-between p-4 rounded-lg shadow text-white bg-{{ 'red' if category == 'danger' else 'green' }}-500"
|
<div class="flex items-center justify-between p-4 rounded-xl shadow-xl text-white bg-{{ 'red' if category == 'danger' else 'primary' }}-500 min-w-[300px]"
|
||||||
x-data="{ visible: true }" x-show="visible" x-transition.duration.300ms>
|
x-data="{ visible: true }" x-show="visible" x-transition.duration.300ms>
|
||||||
<span>{{ message }}</span>
|
<span class="font-medium">{{ message }}</span>
|
||||||
<button @click="visible = false" class="text-xl font-bold">×</button>
|
<button @click="visible = false"
|
||||||
|
class="text-2xl font-bold ml-4 hover:text-gray-200 transition-colors">×</button>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,88 @@
|
|||||||
{% extends "_layout.html" %}
|
{% extends "_layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
|
<div class="max-w-md mx-auto mt-12 mb-20">
|
||||||
<h1 class="text-2xl font-bold text-center mb-4">Login</h1>
|
<div class="bg-white p-10 rounded-3xl shadow-2xl border border-gray-100 relative overflow-hidden">
|
||||||
|
<!-- Decorative background element -->
|
||||||
|
<div class="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-primary-400 to-primary-600"></div>
|
||||||
|
|
||||||
|
<h1 class="text-3xl font-extrabold text-center mb-2 text-gray-900">Welcome Back</h1>
|
||||||
|
<p class="text-center text-gray-500 mb-8">Please enter your details to sign in.</p>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('auth.login') }}" novalidate>
|
<form method="POST" action="{{ url_for('auth.login') }}" novalidate>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<!-- Username Field -->
|
<!-- Username Field -->
|
||||||
<div class="mb-4">
|
<div class="mb-5">
|
||||||
{{ form.username.label(class="block text-sm font-medium text-gray-700") }}
|
{{ form.username.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||||
{{ form.username(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{{ form.username(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||||
|
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||||
|
focus:bg-white") }}
|
||||||
|
</div>
|
||||||
{% for error in form.username.errors %}
|
{% for error in form.username.errors %}
|
||||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password Field -->
|
<!-- Password Field -->
|
||||||
<div class="mb-4">
|
<div class="mb-5">
|
||||||
{{ form.password.label(class="block text-sm font-medium text-gray-700") }}
|
<div class="flex justify-between items-center mb-2">
|
||||||
{{ form.password(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
{{ form.password.label(class="block text-sm font-semibold text-gray-700") }}
|
||||||
|
<a href="#"
|
||||||
|
class="text-sm text-primary-600 hover:text-primary-800 hover:underline font-medium">Forgot
|
||||||
|
Password?</a>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{{ form.password(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||||
|
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||||
|
focus:bg-white") }}
|
||||||
|
</div>
|
||||||
{% for error in form.password.errors %}
|
{% for error in form.password.errors %}
|
||||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<div>
|
<div class="mt-8">
|
||||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
{{ form.submit(class="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold py-3 rounded-xl
|
||||||
|
shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-0.5 cursor-pointer")
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center mt-6 text-gray-600">
|
||||||
|
Don't have an account? <a href="{{ url_for('auth.signup') }}"
|
||||||
|
class="text-primary-600 hover:text-primary-800 font-bold hover:underline">Sign up</a>
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,42 +1,111 @@
|
|||||||
{% extends "_layout.html" %}
|
{% extends "_layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
|
<div class="max-w-md mx-auto mt-12 mb-20">
|
||||||
<h1 class="text-2xl font-bold text-center mb-4">Sign Up</h1>
|
<div class="bg-white p-10 rounded-3xl shadow-2xl border border-gray-100 relative overflow-hidden">
|
||||||
|
<!-- Decorative background element -->
|
||||||
|
<div class="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-primary-400 to-primary-600"></div>
|
||||||
|
|
||||||
|
<h1 class="text-3xl font-extrabold text-center mb-2 text-gray-900">Create Account</h1>
|
||||||
|
<p class="text-center text-gray-500 mb-8">Join us to start tracking your health.</p>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('auth.signup') }}" novalidate>
|
<form method="POST" action="{{ url_for('auth.signup') }}" novalidate>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<!-- Username Field -->
|
<!-- Username Field -->
|
||||||
<div class="mb-4">
|
<div class="mb-5">
|
||||||
{{ form.username.label(class="block text-sm font-medium text-gray-700") }}
|
{{ form.username.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||||
{{ form.username(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{{ form.username(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||||
|
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||||
|
focus:bg-white") }}
|
||||||
|
</div>
|
||||||
{% for error in form.username.errors %}
|
{% for error in form.username.errors %}
|
||||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Password Field -->
|
<!-- Password Field -->
|
||||||
<div class="mb-4">
|
<div class="mb-5">
|
||||||
{{ form.password.label(class="block text-sm font-medium text-gray-700") }}
|
{{ form.password.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||||
{{ form.password(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{{ form.password(class="w-full pl-11 p-3 border border-gray-300 rounded-xl focus:outline-none
|
||||||
|
focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all shadow-sm bg-gray-50
|
||||||
|
focus:bg-white") }}
|
||||||
|
</div>
|
||||||
{% for error in form.password.errors %}
|
{% for error in form.password.errors %}
|
||||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Password Field -->
|
<!-- Confirm Password Field -->
|
||||||
<div class="mb-4">
|
<div class="mb-6">
|
||||||
{{ form.confirm_password.label(class="block text-sm font-medium text-gray-700") }}
|
{{ form.confirm_password.label(class="block text-sm font-semibold text-gray-700 mb-2") }}
|
||||||
{{ form.confirm_password(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
<div class="relative">
|
||||||
focus:ring-blue-500") }}
|
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{{ form.confirm_password(class="w-full pl-11 p-3 border border-gray-300 rounded-xl
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all
|
||||||
|
shadow-sm bg-gray-50 focus:bg-white") }}
|
||||||
|
</div>
|
||||||
{% for error in form.confirm_password.errors %}
|
{% for error in form.confirm_password.errors %}
|
||||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
<p class="text-sm text-red-500 mt-2 font-medium flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<div>
|
<div class="mt-8">
|
||||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
{{ form.submit(class="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold py-3 rounded-xl
|
||||||
|
shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-0.5 cursor-pointer")
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="text-center mt-6 text-gray-600">
|
||||||
|
Already have an account? <a href="{{ url_for('auth.login') }}"
|
||||||
|
class="text-primary-600 hover:text-primary-800 font-bold hover:underline">Log in</a>
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
{% extends "_layout.html" %}
|
{% extends "_layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-5xl mx-auto p-4 space-y-6">
|
<div class="max-w-5xl mx-auto p-4 space-y-6">
|
||||||
|
|
||||||
<!-- Header Section with "Add New Reading" Button -->
|
<!-- Header Section with "Add New Reading" Button -->
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h1 class="text-2xl font-bold text-gray-800">Dashboard</h1>
|
<h1 class="text-2xl font-bold text-gray-800">Dashboard</h1>
|
||||||
<a rel="prefetch" href="{{ url_for('reading.add_reading') }}"
|
<a rel="prefetch" href="{{ url_for('reading.add_reading') }}"
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded shadow hover:bg-blue-700">
|
class="bg-primary-600 text-white px-4 py-2 rounded shadow hover:bg-primary-700">
|
||||||
+ Add New Reading
|
+ Add New Reading
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Weekly Summary -->
|
<!-- Weekly Summary -->
|
||||||
<div class="bg-gradient-to-r from-blue-500 to-blue-700 text-white p-6 rounded-lg shadow-md">
|
<div class="bg-gradient-to-r from-primary-500 to-primary-700 text-white p-6 rounded-xl shadow-md">
|
||||||
<h3 class="text-lg font-bold">Weekly Summary</h3>
|
<h3 class="text-lg font-bold">Weekly Summary</h3>
|
||||||
<div class="flex justify-between mt-4">
|
<div class="flex justify-between mt-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -32,7 +31,6 @@
|
|||||||
|
|
||||||
<!-- Progress Badges -->
|
<!-- Progress Badges -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-bold text-gray-800 mb-2">Progress Badges</h3>
|
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
{% for badge in badges %}
|
{% for badge in badges %}
|
||||||
<div class="bg-green-100 text-green-800 px-4 py-2 rounded shadow text-sm font-medium">
|
<div class="bg-green-100 text-green-800 px-4 py-2 rounded shadow text-sm font-medium">
|
||||||
@@ -42,56 +40,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-data="{ open: {{ 'true' if request.method == 'POST' else 'false' }} }"
|
<div x-data="{ open: {{ 'true' if request.method == 'POST' else 'false' }} }" class="relative">
|
||||||
class="p-4 bg-white rounded-lg shadow-md">
|
<!-- Compact Icon -->
|
||||||
<!-- Collapsible Header -->
|
<button @click="open = !open"
|
||||||
<div class="flex justify-between items-center">
|
class="bg-primary-600 text-white p-3 rounded-full shadow-lg focus:outline-none hover:bg-primary-700">
|
||||||
<h3 class="text-lg font-bold text-gray-800">Filter Readings</h3>
|
|
||||||
<button @click="open = !open" class="text-blue-600 hover:underline flex items-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||||
stroke="currentColor" class="w-4 h-4 mr-2">
|
stroke="currentColor" class="w-6 h-6">
|
||||||
<path x-show="!open" stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
|
<path x-show="!open" stroke-linecap="round" stroke-linejoin="round" d="M6 9l6 6 6-6" />
|
||||||
<path x-show="open" x-cloak stroke-linecap="round" stroke-linejoin="round" d="M18 15l-6-6-6 6" />
|
<path x-show="open" x-cloak stroke-linecap="round" stroke-linejoin="round" d="M18 15l-6-6-6 6" />
|
||||||
</svg>
|
</svg>
|
||||||
<span x-show="!open">Show Filters</span>
|
|
||||||
<span x-show="open" x-cloak>Hide Filters</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Collapsible Content -->
|
<!-- Collapsible Filter Form -->
|
||||||
<form method="POST" action="{{ url_for('main.dashboard') }}" x-show="open" x-transition.duration.50ms
|
<div x-show="open" x-transition.duration.300ms
|
||||||
class="mt-4">
|
class="w-full md:w-1/3 bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-bold text-gray-800">Filter Readings</h3>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ url_for('main.dashboard') }}" class="space-y-4">
|
||||||
|
<!-- Start Date -->
|
||||||
<div>
|
<div>
|
||||||
<label for="start_date" class="block text-sm font-medium text-gray-700">Start Date</label>
|
<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" value="{{ start_date or '' }}"
|
<input type="date" name="start_date" id="start_date"
|
||||||
class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
class="w-full p-3 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- End Date -->
|
||||||
<div>
|
<div>
|
||||||
<label for="end_date" class="block text-sm font-medium text-gray-700">End Date</label>
|
<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" value="{{ end_date or '' }}"
|
<input type="date" name="end_date" id="end_date"
|
||||||
class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
class="w-full p-3 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="mt-4">
|
<!-- Apply Button -->
|
||||||
|
<div>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="w-full md:w-auto bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
class="w-full bg-primary-600 text-white py-2 rounded-xl font-semibold shadow-md hover:bg-primary-700 focus:outline-none">
|
||||||
Apply Filters
|
Apply Filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="max-w-5xl mx-auto" x-data="{ activeView: 'list' }">
|
<div class="max-w-5xl mx-auto" x-data="{ activeView: 'list' }">
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="flex border-b mb-4">
|
<div class="flex border-b mb-4">
|
||||||
<button @click="activeView = 'list'" :class="{'border-blue-600 text-blue-600': activeView === 'list'}"
|
<button @click="activeView = 'list'" :class="{'border-primary-600 text-primary-600': activeView === 'list'}"
|
||||||
class="px-4 py-2 text-sm font-medium border-b-2">List View</button>
|
class="px-4 py-2 text-sm font-medium border-b-2">List View</button>
|
||||||
<button @click="activeView = 'weekly'" :class="{'border-blue-600 text-blue-600': activeView === 'weekly'}"
|
<button @click="activeView = 'weekly'"
|
||||||
|
:class="{'border-primary-600 text-primary-600': activeView === 'weekly'}"
|
||||||
class="px-4 py-2 text-sm font-medium border-b-2">Weekly View</button>
|
class="px-4 py-2 text-sm font-medium border-b-2">Weekly View</button>
|
||||||
<button @click="activeView = 'monthly'" :class="{'border-blue-600 text-blue-600': activeView === 'monthly'}"
|
<button @click="activeView = 'monthly'"
|
||||||
|
:class="{'border-primary-600 text-primary-600': activeView === 'monthly'}"
|
||||||
class="px-4 py-2 text-sm font-medium border-b-2">Monthly View</button>
|
class="px-4 py-2 text-sm font-medium border-b-2">Monthly View</button>
|
||||||
<button @click="activeView = 'graph'" :class="{'border-blue-600 text-blue-600': activeView === 'graph'}"
|
<button @click="activeView = 'graph'"
|
||||||
|
:class="{'border-primary-600 text-primary-600': activeView === 'graph'}"
|
||||||
class="px-4 py-2 text-sm font-medium border-b-2">Graph View</button>
|
class="px-4 py-2 text-sm font-medium border-b-2">Graph View</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,7 +104,7 @@
|
|||||||
<div x-show="activeView === 'list'" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div x-show="activeView === 'list'" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{% for reading in readings %}
|
{% for reading in readings %}
|
||||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||||
class="bg-white shadow-md rounded-lg p-4 flex flex-col justify-between relative hover:shadow-lg transition-shadow">
|
class="bg-white shadow-md rounded-xl p-4 flex flex-col justify-between relative hover:shadow-lg transition-shadow">
|
||||||
<!-- Timestamp -->
|
<!-- Timestamp -->
|
||||||
<div class="absolute top-2 right-2 flex items-center text-gray-400 text-xs">
|
<div class="absolute top-2 right-2 flex items-center text-gray-400 text-xs">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||||
@@ -115,7 +120,7 @@
|
|||||||
<!-- Blood Pressure -->
|
<!-- Blood Pressure -->
|
||||||
<div class="text-sm text-gray-600 mb-2">
|
<div class="text-sm text-gray-600 mb-2">
|
||||||
<span class="block text-lg font-semibold text-gray-800">Blood Pressure</span>
|
<span class="block text-lg font-semibold text-gray-800">Blood Pressure</span>
|
||||||
<span class="text-2xl font-bold text-blue-600">{{ reading.systolic }}</span>
|
<span class="text-2xl font-bold text-primary-600">{{ reading.systolic }}</span>
|
||||||
<span class="text-lg text-gray-500">/</span>
|
<span class="text-lg text-gray-500">/</span>
|
||||||
<span class="text-xl font-bold text-red-600">{{ reading.diastolic }}</span>
|
<span class="text-xl font-bold text-red-600">{{ reading.diastolic }}</span>
|
||||||
<span class="text-sm text-gray-500">mmHg</span>
|
<span class="text-sm text-gray-500">mmHg</span>
|
||||||
@@ -146,6 +151,32 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
{% if pagination.pages > 1 %}
|
||||||
|
<div x-show="activeView === 'list'" class="flex justify-center items-center gap-2 mt-6">
|
||||||
|
{% if pagination.has_prev %}
|
||||||
|
<a href="{{ url_for('main.dashboard', page=pagination.prev_num) }}"
|
||||||
|
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">« Prev</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||||
|
{% if page_num %}
|
||||||
|
<a href="{{ url_for('main.dashboard', page=page_num) }}"
|
||||||
|
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 }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-400">…</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if pagination.has_next %}
|
||||||
|
<a href="{{ url_for('main.dashboard', page=pagination.next_num) }}"
|
||||||
|
class="px-3 py-1 rounded bg-gray-200 hover:bg-gray-300 text-sm">Next »</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Weekly View -->
|
<!-- Weekly View -->
|
||||||
<div x-show="activeView === 'weekly'" class="grid grid-cols-7 text-center">
|
<div x-show="activeView === 'weekly'" class="grid grid-cols-7 text-center">
|
||||||
{% for day in week %}
|
{% for day in week %}
|
||||||
@@ -154,7 +185,7 @@
|
|||||||
{% if day.readings %}
|
{% if day.readings %}
|
||||||
{% for reading in day.readings %}
|
{% for reading in day.readings %}
|
||||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||||
class="block mt-2 p-0 md:p-2 bg-green-100 rounded-lg shadow hover:bg-green-200 transition">
|
class="block mt-2 p-0 md:p-2 bg-green-100 rounded-xl shadow hover:bg-green-200 transition">
|
||||||
<p class="text-xs font-medium text-green-800">
|
<p class="text-xs font-medium text-green-800">
|
||||||
{{ reading.systolic }}/{{ reading.diastolic }} mmHg
|
{{ reading.systolic }}/{{ reading.diastolic }} mmHg
|
||||||
</p>
|
</p>
|
||||||
@@ -222,7 +253,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% for reading in day.readings %}
|
{% for reading in day.readings %}
|
||||||
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
<a href="{{ url_for('reading.edit_reading', reading_id=reading.id) }}"
|
||||||
class="block mt-2 p-0 md:p-2 bg-green-100 rounded-lg shadow hover:bg-green-200 transition">
|
class="block mt-2 p-0 md:p-2 bg-green-100 rounded-xl shadow hover:bg-green-200 transition">
|
||||||
<p class="text-xs font-medium text-green-800">
|
<p class="text-xs font-medium text-green-800">
|
||||||
{{ reading.systolic }}/{{ reading.diastolic }} mmHg
|
{{ reading.systolic }}/{{ reading.diastolic }} mmHg
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
{% extends "_layout.html" %}
|
{% extends "_layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="bg-gray-50">
|
<div class="bg-gray-50 pb-20">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="bg-blue-600 text-white">
|
<section
|
||||||
<div class="container mx-auto px-4 py-20 text-center">
|
class="bg-gradient-to-br from-primary-600 to-primary-800 text-white rounded-b-3xl shadow-xl overflow-hidden relative">
|
||||||
<h1 class="text-4xl font-bold mb-6">
|
<div class="absolute inset-0 bg-white/5 pattern-dots pointer-events-none"></div>
|
||||||
|
<div class="container mx-auto px-4 py-24 text-center relative z-10">
|
||||||
|
<h1 class="text-5xl md:text-6xl font-extrabold mb-6 tracking-tight">
|
||||||
Welcome to BP Tracker
|
Welcome to BP Tracker
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg mb-8">
|
<p class="text-xl md:text-2xl mb-12 max-w-2xl mx-auto font-medium text-primary-50 leading-relaxed">
|
||||||
Track your blood pressure and heart rate effortlessly. Take control of your health today!
|
Track your blood pressure and heart rate effortlessly. Take control of your health today!
|
||||||
</p>
|
</p>
|
||||||
<div class="flex justify-center space-x-4">
|
<div class="flex flex-col sm:flex-row justify-center space-y-4 sm:space-y-0 sm:space-x-6">
|
||||||
<a href="{{ url_for('auth.signup') }}"
|
<a href="{{ url_for('auth.signup') }}"
|
||||||
class="px-8 py-3 bg-white text-blue-600 font-semibold rounded-lg shadow hover:bg-gray-200">
|
class="px-8 py-4 bg-white text-primary-700 font-bold text-lg rounded-xl shadow-xl hover:shadow-2xl hover:bg-gray-50 transition-all duration-300 transform hover:-translate-y-1">
|
||||||
Get Started
|
Get Started
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('auth.login') }}"
|
<a href="{{ url_for('auth.login') }}"
|
||||||
class="px-8 py-3 bg-blue-700 text-white font-semibold rounded-lg shadow hover:bg-blue-800">
|
class="px-8 py-4 bg-primary-700 text-white border border-primary-500 font-bold text-lg rounded-xl shadow-lg hover:shadow-xl hover:bg-primary-600 transition-all duration-300 transform hover:-translate-y-1">
|
||||||
Login
|
Login
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,35 +26,56 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Features Section -->
|
<!-- Features Section -->
|
||||||
<section class="container mx-auto px-4 py-16">
|
<section class="container mx-auto px-4 pt-24">
|
||||||
<h2 class="text-3xl font-bold text-center mb-8">Why Choose BP Tracker?</h2>
|
<div class="text-center mb-16">
|
||||||
<div class="grid md:grid-cols-3 gap-8">
|
<span class="text-primary-600 font-bold tracking-wider uppercase text-sm mb-2 block">Why Track With
|
||||||
<div class="text-center bg-white p-6 rounded-lg shadow">
|
Us</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 text-blue-600 mx-auto mb-4" fill="none"
|
<h2 class="text-4xl font-extrabold text-gray-900 tracking-tight">Why Choose BP Tracker?</h2>
|
||||||
viewBox="0 0 24 24" stroke="currentColor">
|
</div>
|
||||||
|
<div class="grid md:grid-cols-3 gap-10">
|
||||||
|
<div
|
||||||
|
class="text-center bg-white p-10 rounded-3xl shadow-lg border border-gray-100 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
|
||||||
|
<div
|
||||||
|
class="w-20 h-20 mx-auto bg-primary-50 rounded-2xl flex items-center justify-center mb-6 shadow-inner text-primary-600">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M12 8c2.21 0 4 1.79 4 4s-1.79 4-4 4-4-1.79-4-4 1.79-4 4-4z"></path>
|
d="M12 8c2.21 0 4 1.79 4 4s-1.79 4-4 4-4-1.79-4-4 1.79-4 4-4z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="text-lg font-semibold mb-2">Accurate Tracking</h3>
|
|
||||||
<p>Keep a detailed log of your blood pressure and heart rate over time.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center bg-white p-6 rounded-lg shadow">
|
<h3 class="text-xl font-bold text-gray-900 mb-3">Accurate Tracking</h3>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 text-blue-600 mx-auto mb-4" fill="none"
|
<p class="text-gray-600 leading-relaxed">Keep a detailed log of your blood pressure and heart rate over
|
||||||
viewBox="0 0 24 24" stroke="currentColor">
|
time to share with your doctor.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-center bg-white p-10 rounded-3xl shadow-lg border border-gray-100 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
|
||||||
|
<div
|
||||||
|
class="w-20 h-20 mx-auto bg-primary-50 rounded-2xl flex items-center justify-center mb-6 shadow-inner text-primary-600">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M9.75 16.5L15 12m0 0l-5.25-4.5m5.25 4.5H3"></path>
|
d="M9.75 16.5L15 12m0 0l-5.25-4.5m5.25 4.5H3"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="text-lg font-semibold mb-2">Insightful Graphs</h3>
|
|
||||||
<p>Visualize your progress and identify trends with intuitive charts.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center bg-white p-6 rounded-lg shadow">
|
<h3 class="text-xl font-bold text-gray-900 mb-3">Insightful Graphs</h3>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 text-blue-600 mx-auto mb-4" fill="none"
|
<p class="text-gray-600 leading-relaxed">Visualize your progress and identify health trends with our
|
||||||
viewBox="0 0 24 24" stroke="currentColor">
|
intuitive, easy-to-read charts.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-center bg-white p-10 rounded-3xl shadow-lg border border-gray-100 hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2">
|
||||||
|
<div
|
||||||
|
class="w-20 h-20 mx-auto bg-primary-50 rounded-2xl flex items-center justify-center mb-6 shadow-inner text-primary-600">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M9 20l-5.5-5.5M9 20V9m0 11h11"></path>
|
d="M9 20l-5.5-5.5M9 20V9m0 11h11"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="text-lg font-semibold mb-2">Secure and Private</h3>
|
</div>
|
||||||
<p>Your data is protected with state-of-the-art security measures.</p>
|
<h3 class="text-xl font-bold text-gray-900 mb-3">Secure and Private</h3>
|
||||||
|
<p class="text-gray-600 leading-relaxed">Your medical data is protected with state-of-the-art security
|
||||||
|
measures. Your privacy is paramount.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""Add reading index and profile updated_at
|
||||||
|
|
||||||
|
Revision ID: 8cfe56a1e597
|
||||||
|
Revises: 5e5a1b78b966
|
||||||
|
Create Date: 2026-03-09 21:21:44.291659
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '8cfe56a1e597'
|
||||||
|
down_revision = '5e5a1b78b966'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('profile', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
with op.batch_alter_table('reading', schema=None) as batch_op:
|
||||||
|
batch_op.create_index('ix_reading_user_timestamp', ['user_id', 'timestamp'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('reading', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index('ix_reading_user_timestamp')
|
||||||
|
|
||||||
|
with op.batch_alter_table('profile', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('updated_at')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -2,7 +2,25 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./app/templates/**/*.html"],
|
content: ["./app/templates/**/*.html"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0fdfa',
|
||||||
|
100: '#ccfbf1',
|
||||||
|
200: '#99f6e4',
|
||||||
|
300: '#5eead4',
|
||||||
|
400: '#2dd4bf',
|
||||||
|
500: '#14b8a6', // Teal MedTech feeling
|
||||||
|
600: '#0d9488',
|
||||||
|
700: '#0f766e',
|
||||||
|
800: '#115e59',
|
||||||
|
900: '#134e4a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user