add habit leveling/XP system, explicit fail marking for missed habits, and scrollable habit tabs
- Habits and the account now accumulate XP and levels from check streaks, with streak/level-up celebration banners and push notifications. - The "yesterday missed" banner on /today now lets a habit be explicitly marked failed (not just checked done), backed by a new HabitLog.status column so completion-rate/streak calculations never count a failed day as done. - The habit management tabs scroll horizontally on narrow screens instead of wrapping to two lines, with a pure-CSS edge shadow indicating more content.
This commit is contained in:
+50
-6
@@ -14,7 +14,14 @@ from app.models.user import User
|
||||
from app.schemas.habit import HabitCreate, HabitUpdate
|
||||
from app.security import get_current_user_optional
|
||||
from app.services import habit_service, log_service
|
||||
from app.template_utils import difficulty_label, goal_progress, heatmap_opacity, is_milestone_streak, weekday_label
|
||||
from app.template_utils import (
|
||||
difficulty_label,
|
||||
goal_progress,
|
||||
heatmap_opacity,
|
||||
is_milestone_streak,
|
||||
level_tier_emoji,
|
||||
weekday_label,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -24,6 +31,7 @@ templates.env.globals["heatmap_opacity"] = heatmap_opacity
|
||||
templates.env.globals["is_milestone_streak"] = is_milestone_streak
|
||||
templates.env.globals["difficulty_label"] = difficulty_label
|
||||
templates.env.globals["goal_progress"] = goal_progress
|
||||
templates.env.globals["level_tier_emoji"] = level_tier_emoji
|
||||
templates.env.globals["habit_difficulties"] = list(HabitDifficulty)
|
||||
|
||||
|
||||
@@ -57,6 +65,7 @@ def _today_context(db: Session, user_id: int, **extra) -> dict:
|
||||
"yesterday_missed": log_service.get_yesterday_missed_items(db, user_id),
|
||||
"total_count": len(all_items),
|
||||
"checked_count": sum(1 for item in all_items if item.checked),
|
||||
"account_level": log_service.get_account_level(db, user_id),
|
||||
**extra,
|
||||
}
|
||||
|
||||
@@ -84,8 +93,12 @@ def toggle_today_page(request: Request, habit_id: int, db: Session = Depends(get
|
||||
if habit is None:
|
||||
return Response(status_code=404)
|
||||
|
||||
_, milestone_streak = log_service.toggle_check_and_celebrate(db, habit, date.today())
|
||||
extra = {"celebrate_habit_name": habit.name, "celebrate_streak": milestone_streak} if milestone_streak else {}
|
||||
_, milestone_streak, level_up = log_service.toggle_check_and_celebrate(db, habit, date.today())
|
||||
extra = {"celebrate_habit_name": habit.name} if (milestone_streak or level_up) else {}
|
||||
if milestone_streak:
|
||||
extra["celebrate_streak"] = milestone_streak
|
||||
if level_up:
|
||||
extra["celebrate_level_up"] = level_up
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/today_content.html",
|
||||
@@ -104,8 +117,12 @@ def toggle_yesterday_page(request: Request, habit_id: int, db: Session = Depends
|
||||
return Response(status_code=404)
|
||||
|
||||
yesterday = date.today() - timedelta(days=1)
|
||||
_, milestone_streak = log_service.toggle_check_and_celebrate(db, habit, yesterday)
|
||||
extra = {"celebrate_habit_name": habit.name, "celebrate_streak": milestone_streak} if milestone_streak else {}
|
||||
_, milestone_streak, level_up = log_service.toggle_check_and_celebrate(db, habit, yesterday)
|
||||
extra = {"celebrate_habit_name": habit.name} if (milestone_streak or level_up) else {}
|
||||
if milestone_streak:
|
||||
extra["celebrate_streak"] = milestone_streak
|
||||
if level_up:
|
||||
extra["celebrate_level_up"] = level_up
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/today_content.html",
|
||||
@@ -113,6 +130,25 @@ def toggle_yesterday_page(request: Request, habit_id: int, db: Session = Depends
|
||||
)
|
||||
|
||||
|
||||
@router.post("/today/{habit_id}/fail-yesterday")
|
||||
def fail_yesterday_page(request: Request, habit_id: int, db: Session = Depends(get_db)):
|
||||
current = _current_user_or_redirect(request, db)
|
||||
if isinstance(current, RedirectResponse):
|
||||
return current
|
||||
|
||||
habit = habit_service.get_habit(db, habit_id, current.id)
|
||||
if habit is None:
|
||||
return Response(status_code=404)
|
||||
|
||||
yesterday = date.today() - timedelta(days=1)
|
||||
log_service.mark_failed(db, habit.id, yesterday)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"partials/today_content.html",
|
||||
{"logged_in": True, "current_user": current, **_today_context(db, current.id)},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/habits")
|
||||
def habits_page(request: Request, tab: str = "build", db: Session = Depends(get_db)):
|
||||
current = _current_user_or_redirect(request, db)
|
||||
@@ -140,6 +176,7 @@ def habits_page(request: Request, tab: str = "build", db: Session = Depends(get_
|
||||
"habits": habits,
|
||||
"habit_type": tab,
|
||||
"stats_map": stats_map,
|
||||
"account_level": log_service.get_account_level(db, current.id),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -355,5 +392,12 @@ def history_page(
|
||||
context = _month_context(db, current.id, year or today.year, month or today.month)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request, "history.html", {"logged_in": True, "current_user": current, **context}
|
||||
request,
|
||||
"history.html",
|
||||
{
|
||||
"logged_in": True,
|
||||
"current_user": current,
|
||||
"account_level": log_service.get_account_level(db, current.id),
|
||||
**context,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user