Show daily streak on dashboard

This commit is contained in:
Peter Stockings
2026-02-24 20:58:04 +11:00
parent 1c935a64be
commit 9323082d37
3 changed files with 143 additions and 7 deletions

View File

@@ -1,10 +1,51 @@
from flask import Blueprint, render_template
from app.auth import login_required, get_current_user
from app.db import query, query_one
from app import SYDNEY_TZ
from datetime import datetime, timezone, timedelta
bp = Blueprint("dashboard", __name__)
def calculate_streak(user_id):
"""Calculate current and best consecutive-day check-in streaks."""
rows = query(
"""SELECT DISTINCT (checked_in_at AT TIME ZONE 'UTC' AT TIME ZONE 'Australia/Sydney')::date AS d
FROM checkins WHERE user_id = %s ORDER BY d DESC""",
(user_id,),
)
if not rows:
return {"current": 0, "best": 0}
days = [r["d"] for r in rows]
today = datetime.now(SYDNEY_TZ).date()
# Current streak: must include today or yesterday to count
current = 0
expected = today
if days[0] == today or days[0] == today - timedelta(days=1):
expected = days[0]
for d in days:
if d == expected:
current += 1
expected -= timedelta(days=1)
else:
break
# Best streak
best = 1
run = 1
for i in range(1, len(days)):
if days[i] == days[i - 1] - timedelta(days=1):
run += 1
best = max(best, run)
else:
run = 1
best = max(best, current)
return {"current": current, "best": best}
@bp.route("/")
@login_required
def index():
@@ -59,6 +100,9 @@ def index():
(user["id"],),
)
# Streak
streak = calculate_streak(user["id"])
return render_template(
"dashboard.html",
user=user,
@@ -69,4 +113,6 @@ def index():
recent_checkins=recent_checkins,
activity=activity,
milestones=milestones,
streak=streak,
)

View File

@@ -294,6 +294,11 @@ body {
opacity: 1;
}
.stat-card-streak::before {
background: linear-gradient(135deg, #f59e0b, #ef4444);
opacity: 1;
}
.stat-label {
font-size: 0.75rem;
font-weight: 500;
@@ -737,6 +742,42 @@ tr:hover td {
border-color: rgba(239, 68, 68, 0.2);
}
.btn-icon-success:hover {
color: var(--success) !important;
background: var(--success-bg) !important;
border-color: rgba(16, 185, 129, 0.2) !important;
}
/* ========== INLINE EDIT ========== */
.checkin-actions {
display: flex;
gap: 0.25rem;
align-items: center;
}
.edit-input {
width: 100%;
max-width: 120px;
padding: 0.35rem 0.6rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: inherit;
font-size: 0.85rem;
transition: all 0.2s ease;
outline: none;
}
.edit-input:focus {
border-color: var(--border-focus);
box-shadow: 0 0 0 2px var(--accent-glow);
}
.editing-row td {
background: var(--bg-card-hover);
}
/* ========== EMPTY STATE ========== */
.empty-state {
text-align: center;
@@ -933,20 +974,46 @@ tr:hover td {
}
.container {
padding: 1rem;
padding: 0.75rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
margin-bottom: 1rem;
}
.stat-card {
padding: 0.85rem;
}
.stat-value {
font-size: 1.35rem;
}
.stat-label {
font-size: 0.65rem;
margin-bottom: 0.25rem;
}
.charts-grid {
grid-template-columns: 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
.chart-container {
height: 220px;
}
.grid-2,
.grid-3 {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.card {
padding: 1rem;
}
.form-inline {
@@ -958,12 +1025,16 @@ tr:hover td {
margin-bottom: 0.75rem;
}
.page-header h1 {
font-size: 1.4rem;
.page-header {
margin-bottom: 1rem;
}
.stat-value {
font-size: 1.4rem;
.page-header h1 {
font-size: 1.3rem;
}
.page-header p {
font-size: 0.8rem;
}
.auth-card {
@@ -998,10 +1069,27 @@ tr:hover td {
.filter-group-people {
min-width: unset;
}
.card-header h2 {
font-size: 0.9rem;
}
.activity-item {
padding: 0.5rem 0;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.stat-card {
padding: 0.7rem;
}
.stat-value {
font-size: 1.15rem;
}
}

View File

@@ -34,9 +34,11 @@
<div class="stat-label">Current BMI</div>
<div class="stat-value">{{ '%.1f' % (latest.bmi | float) if latest and latest.bmi else '—' }}</div>
</div>
<div class="stat-card">
<div class="stat-card {{ 'stat-card-streak' if streak.current >= 3 }}">
<div class="stat-label">Check-ins</div>
<div class="stat-value">{{ stats.total_checkins if stats else 0 }}</div>
<div class="stat-change" style="color: var(--warning);">🔥 {{ streak.current }}-day streak{% if streak.best >
streak.current %} · Best: {{ streak.best }}{% endif %}</div>
</div>
</div>