Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import plotly.express as px
|
| 8 |
+
import plotly.graph_objects as go
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
import base64
|
| 11 |
+
|
| 12 |
+
# Page configuration
|
| 13 |
+
st.set_page_config(
|
| 14 |
+
page_title="Atomic Habits Tracker Pro",
|
| 15 |
+
page_icon="π₯",
|
| 16 |
+
layout="wide",
|
| 17 |
+
initial_sidebar_state="expanded"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Categories with emojis
|
| 21 |
+
CATEGORIES = {
|
| 22 |
+
"Health": "πͺ",
|
| 23 |
+
"Learning": "π",
|
| 24 |
+
"Mindfulness": "π§",
|
| 25 |
+
"Productivity": "β‘",
|
| 26 |
+
"Creativity": "π¨",
|
| 27 |
+
"Social": "π₯",
|
| 28 |
+
"Finance": "π°",
|
| 29 |
+
"Other": "πΉ"
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
# Initialize session state for theme
|
| 33 |
+
if 'dark_mode' not in st.session_state:
|
| 34 |
+
st.session_state.dark_mode = False
|
| 35 |
+
|
| 36 |
+
# Theme toggle function
|
| 37 |
+
def toggle_theme():
|
| 38 |
+
st.session_state.dark_mode = not st.session_state.dark_mode
|
| 39 |
+
|
| 40 |
+
# Dynamic CSS based on theme
|
| 41 |
+
def get_css():
|
| 42 |
+
if st.session_state.dark_mode:
|
| 43 |
+
return """
|
| 44 |
+
<style>
|
| 45 |
+
.main-header {
|
| 46 |
+
font-size: 3rem;
|
| 47 |
+
font-weight: bold;
|
| 48 |
+
background: linear-gradient(90deg, #FF6B6B, #4ECDC4);
|
| 49 |
+
-webkit-background-clip: text;
|
| 50 |
+
-webkit-text-fill-color: transparent;
|
| 51 |
+
text-align: center;
|
| 52 |
+
margin-bottom: 2rem;
|
| 53 |
+
}
|
| 54 |
+
.stApp {
|
| 55 |
+
background-color: #0e1117;
|
| 56 |
+
color: #fafafa;
|
| 57 |
+
}
|
| 58 |
+
.quote-box {
|
| 59 |
+
background: #1e1e1e;
|
| 60 |
+
border-left: 5px solid #4ECDC4;
|
| 61 |
+
padding: 15px;
|
| 62 |
+
margin: 20px 0;
|
| 63 |
+
font-style: italic;
|
| 64 |
+
color: #fafafa;
|
| 65 |
+
border-radius: 0 10px 10px 0;
|
| 66 |
+
}
|
| 67 |
+
.habit-card {
|
| 68 |
+
background: #1e1e1e;
|
| 69 |
+
padding: 20px;
|
| 70 |
+
border-radius: 15px;
|
| 71 |
+
margin: 10px 0;
|
| 72 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
| 73 |
+
border: 1px solid #333;
|
| 74 |
+
}
|
| 75 |
+
.streak-badge {
|
| 76 |
+
background: #FFD93D;
|
| 77 |
+
color: #333;
|
| 78 |
+
padding: 5px 15px;
|
| 79 |
+
border-radius: 20px;
|
| 80 |
+
font-weight: bold;
|
| 81 |
+
display: inline-block;
|
| 82 |
+
margin: 5px;
|
| 83 |
+
}
|
| 84 |
+
.category-badge {
|
| 85 |
+
background: #333;
|
| 86 |
+
color: #fff;
|
| 87 |
+
padding: 3px 10px;
|
| 88 |
+
border-radius: 15px;
|
| 89 |
+
font-size: 0.8rem;
|
| 90 |
+
display: inline-block;
|
| 91 |
+
margin: 5px 0;
|
| 92 |
+
}
|
| 93 |
+
.stButton>button {
|
| 94 |
+
border-radius: 25px;
|
| 95 |
+
transition: all 0.3s;
|
| 96 |
+
}
|
| 97 |
+
.stTextInput>div>div>input, .stSelectbox>div>div>select {
|
| 98 |
+
background-color: #1e1e1e;
|
| 99 |
+
color: #fafafa;
|
| 100 |
+
border-radius: 10px;
|
| 101 |
+
}
|
| 102 |
+
.stProgress > div > div > div > div {
|
| 103 |
+
background-color: #4ECDC4;
|
| 104 |
+
}
|
| 105 |
+
div[data-testid="stSidebar"] {
|
| 106 |
+
background-color: #1e1e1e;
|
| 107 |
+
}
|
| 108 |
+
h1, h2, h3, h4, h5, h6, p, label {
|
| 109 |
+
color: #fafafa !important;
|
| 110 |
+
}
|
| 111 |
+
.footer-text {
|
| 112 |
+
color: #888 !important;
|
| 113 |
+
}
|
| 114 |
+
</style>
|
| 115 |
+
"""
|
| 116 |
+
else:
|
| 117 |
+
return """
|
| 118 |
+
<style>
|
| 119 |
+
.main-header {
|
| 120 |
+
font-size: 3rem;
|
| 121 |
+
font-weight: bold;
|
| 122 |
+
background: linear-gradient(90deg, #FF6B6B, #4ECDC4);
|
| 123 |
+
-webkit-background-clip: text;
|
| 124 |
+
-webkit-text-fill-color: transparent;
|
| 125 |
+
text-align: center;
|
| 126 |
+
margin-bottom: 2rem;
|
| 127 |
+
}
|
| 128 |
+
.quote-box {
|
| 129 |
+
background: #f0f2f6;
|
| 130 |
+
border-left: 5px solid #4ECDC4;
|
| 131 |
+
padding: 15px;
|
| 132 |
+
margin: 20px 0;
|
| 133 |
+
font-style: italic;
|
| 134 |
+
border-radius: 0 10px 10px 0;
|
| 135 |
+
}
|
| 136 |
+
.habit-card {
|
| 137 |
+
background: white;
|
| 138 |
+
padding: 20px;
|
| 139 |
+
border-radius: 15px;
|
| 140 |
+
margin: 10px 0;
|
| 141 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 142 |
+
}
|
| 143 |
+
.streak-badge {
|
| 144 |
+
background: #FFD93D;
|
| 145 |
+
color: #333;
|
| 146 |
+
padding: 5px 15px;
|
| 147 |
+
border-radius: 20px;
|
| 148 |
+
font-weight: bold;
|
| 149 |
+
display: inline-block;
|
| 150 |
+
margin: 5px;
|
| 151 |
+
}
|
| 152 |
+
.category-badge {
|
| 153 |
+
background: #e0e0e0;
|
| 154 |
+
color: #333;
|
| 155 |
+
padding: 3px 10px;
|
| 156 |
+
border-radius: 15px;
|
| 157 |
+
font-size: 0.8rem;
|
| 158 |
+
display: inline-block;
|
| 159 |
+
margin: 5px 0;
|
| 160 |
+
}
|
| 161 |
+
.stButton>button {
|
| 162 |
+
border-radius: 25px;
|
| 163 |
+
transition: all 0.3s;
|
| 164 |
+
}
|
| 165 |
+
.stTextInput>div>div>input, .stSelectbox>div>div>select {
|
| 166 |
+
border-radius: 10px;
|
| 167 |
+
}
|
| 168 |
+
.stProgress > div > div > div > div {
|
| 169 |
+
background-color: #4ECDC4;
|
| 170 |
+
}
|
| 171 |
+
.footer-text {
|
| 172 |
+
color: #666 !important;
|
| 173 |
+
}
|
| 174 |
+
</style>
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
st.markdown(get_css(), unsafe_allow_html=True)
|
| 178 |
+
|
| 179 |
+
# Data persistence
|
| 180 |
+
DATA_FILE = "habits_data.json"
|
| 181 |
+
|
| 182 |
+
def load_data():
|
| 183 |
+
"""Load habits data from file"""
|
| 184 |
+
if os.path.exists(DATA_FILE):
|
| 185 |
+
with open(DATA_FILE, 'r') as f:
|
| 186 |
+
data = json.load(f)
|
| 187 |
+
# Migrate old data to include categories if missing
|
| 188 |
+
for habit in data.get("habits", []):
|
| 189 |
+
if "category" not in habit:
|
| 190 |
+
habit["category"] = "Other"
|
| 191 |
+
if "reminder_time" not in habit:
|
| 192 |
+
habit["reminder_time"] = "09:00"
|
| 193 |
+
return data
|
| 194 |
+
return {"habits": [], "completions": {}, "reminders_sent": {}}
|
| 195 |
+
|
| 196 |
+
def save_data(data):
|
| 197 |
+
"""Save habits data to file"""
|
| 198 |
+
with open(DATA_FILE, 'w') as f:
|
| 199 |
+
json.dump(data, f, indent=2)
|
| 200 |
+
|
| 201 |
+
def get_streak(habit_id, completions, habit_data):
|
| 202 |
+
"""Calculate current streak for a habit"""
|
| 203 |
+
if habit_id not in completions or not completions[habit_id]:
|
| 204 |
+
return 0
|
| 205 |
+
|
| 206 |
+
dates = sorted([datetime.strptime(d, "%Y-%m-%d").date() for d in completions[habit_id]], reverse=True)
|
| 207 |
+
|
| 208 |
+
if not dates:
|
| 209 |
+
return 0
|
| 210 |
+
|
| 211 |
+
today = datetime.now().date()
|
| 212 |
+
streak = 0
|
| 213 |
+
check_date = today
|
| 214 |
+
|
| 215 |
+
if dates[0] == today or dates[0] == today - timedelta(days=1):
|
| 216 |
+
for i, date in enumerate(dates):
|
| 217 |
+
expected_date = today - timedelta(days=i)
|
| 218 |
+
if date == expected_date or (i == 0 and date == today - timedelta(days=1)):
|
| 219 |
+
streak += 1
|
| 220 |
+
else:
|
| 221 |
+
break
|
| 222 |
+
else:
|
| 223 |
+
streak = 0
|
| 224 |
+
|
| 225 |
+
return streak
|
| 226 |
+
|
| 227 |
+
def get_weekly_completion(habit_id, completions):
|
| 228 |
+
"""Get completion rate for last 7 days"""
|
| 229 |
+
if habit_id not in completions:
|
| 230 |
+
return 0
|
| 231 |
+
|
| 232 |
+
today = datetime.now().date()
|
| 233 |
+
week_ago = today - timedelta(days=7)
|
| 234 |
+
|
| 235 |
+
week_completions = sum(
|
| 236 |
+
1 for d in completions[habit_id]
|
| 237 |
+
if week_ago <= datetime.strptime(d, "%Y-%m-%d").date() <= today
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
return (week_completions / 7) * 100
|
| 241 |
+
|
| 242 |
+
def get_monthly_stats(habit_id, completions):
|
| 243 |
+
"""Get monthly completion stats"""
|
| 244 |
+
if habit_id not in completions:
|
| 245 |
+
return 0, 0
|
| 246 |
+
|
| 247 |
+
today = datetime.now().date()
|
| 248 |
+
month_ago = today - timedelta(days=30)
|
| 249 |
+
|
| 250 |
+
month_completions = [
|
| 251 |
+
datetime.strptime(d, "%Y-%m-%d").date()
|
| 252 |
+
for d in completions[habit_id]
|
| 253 |
+
if month_ago <= datetime.strptime(d, "%Y-%m-%d").date() <= today
|
| 254 |
+
]
|
| 255 |
+
|
| 256 |
+
return len(month_completions), (len(month_completions) / 30) * 100
|
| 257 |
+
|
| 258 |
+
def export_to_csv():
|
| 259 |
+
"""Export all habit data to CSV"""
|
| 260 |
+
data = st.session_state.data
|
| 261 |
+
rows = []
|
| 262 |
+
|
| 263 |
+
for habit in data["habits"]:
|
| 264 |
+
habit_id = habit["id"]
|
| 265 |
+
completions = data["completions"].get(habit_id, [])
|
| 266 |
+
|
| 267 |
+
for date in completions:
|
| 268 |
+
rows.append({
|
| 269 |
+
"Habit Name": habit["name"],
|
| 270 |
+
"Category": habit["category"],
|
| 271 |
+
"Emoji": habit["emoji"],
|
| 272 |
+
"Completion Date": date,
|
| 273 |
+
"Created At": habit["created_at"]
|
| 274 |
+
})
|
| 275 |
+
|
| 276 |
+
if not rows:
|
| 277 |
+
# Create summary even if no completions
|
| 278 |
+
for habit in data["habits"]:
|
| 279 |
+
rows.append({
|
| 280 |
+
"Habit Name": habit["name"],
|
| 281 |
+
"Category": habit["category"],
|
| 282 |
+
"Emoji": habit["emoji"],
|
| 283 |
+
"Completion Date": "No completions yet",
|
| 284 |
+
"Created At": habit["created_at"]
|
| 285 |
+
})
|
| 286 |
+
|
| 287 |
+
df = pd.DataFrame(rows)
|
| 288 |
+
return df.to_csv(index=False).encode('utf-8')
|
| 289 |
+
|
| 290 |
+
def check_reminders():
|
| 291 |
+
"""Check and display due reminders"""
|
| 292 |
+
current_time = datetime.now().strftime("%H:%M")
|
| 293 |
+
today = datetime.now().strftime("%Y-%m-%d")
|
| 294 |
+
|
| 295 |
+
reminders_due = []
|
| 296 |
+
|
| 297 |
+
for habit in st.session_state.data["habits"]:
|
| 298 |
+
reminder_time = habit.get("reminder_time", "09:00")
|
| 299 |
+
habit_id = habit["id"]
|
| 300 |
+
|
| 301 |
+
# Check if reminder time matches current time (within 1 minute window)
|
| 302 |
+
if reminder_time == current_time:
|
| 303 |
+
# Check if not already completed today and not already reminded
|
| 304 |
+
completions = st.session_state.data["completions"].get(habit_id, [])
|
| 305 |
+
reminders_sent = st.session_state.data.get("reminders_sent", {})
|
| 306 |
+
|
| 307 |
+
if today not in completions and reminders_sent.get(habit_id) != today:
|
| 308 |
+
reminders_due.append(habit)
|
| 309 |
+
st.session_state.data["reminders_sent"][habit_id] = today
|
| 310 |
+
save_data(st.session_state.data)
|
| 311 |
+
|
| 312 |
+
return reminders_due
|
| 313 |
+
|
| 314 |
+
# Initialize session state
|
| 315 |
+
if 'data' not in st.session_state:
|
| 316 |
+
st.session_state.data = load_data()
|
| 317 |
+
|
| 318 |
+
# Check for reminders
|
| 319 |
+
reminders = check_reminders()
|
| 320 |
+
|
| 321 |
+
# Sidebar
|
| 322 |
+
with st.sidebar:
|
| 323 |
+
# Dark mode toggle at top
|
| 324 |
+
st.button("π Dark Mode" if not st.session_state.dark_mode else "βοΈ Light Mode",
|
| 325 |
+
on_click=toggle_theme, use_container_width=True)
|
| 326 |
+
|
| 327 |
+
st.markdown("---")
|
| 328 |
+
|
| 329 |
+
st.header("β Add New Habit")
|
| 330 |
+
st.markdown("*Make it obvious, make it easy*")
|
| 331 |
+
|
| 332 |
+
with st.form("new_habit"):
|
| 333 |
+
habit_name = st.text_input("Habit Name", placeholder="e.g., Read 1 page")
|
| 334 |
+
habit_category = st.selectbox("Category", list(CATEGORIES.keys()),
|
| 335 |
+
format_func=lambda x: f"{CATEGORIES[x]} {x}")
|
| 336 |
+
habit_color = st.color_picker("Choose Color", "#4ECDC4")
|
| 337 |
+
habit_emoji = st.selectbox("Icon", ["π", "π", "π§", "π§", "πͺ", "πΈ", "π»", "π±", "βοΈ", "π―", "π₯", "π€"])
|
| 338 |
+
reminder_time = st.time_input("β° Daily Reminder", value=datetime.strptime("09:00", "%H:%M").time())
|
| 339 |
+
|
| 340 |
+
submitted = st.form_submit_button("Create Habit", use_container_width=True)
|
| 341 |
+
|
| 342 |
+
if submitted and habit_name:
|
| 343 |
+
habit_id = f"habit_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
| 344 |
+
new_habit = {
|
| 345 |
+
"id": habit_id,
|
| 346 |
+
"name": habit_name,
|
| 347 |
+
"category": habit_category,
|
| 348 |
+
"color": habit_color,
|
| 349 |
+
"emoji": habit_emoji,
|
| 350 |
+
"reminder_time": reminder_time.strftime("%H:%M"),
|
| 351 |
+
"created_at": datetime.now().strftime("%Y-%m-%d")
|
| 352 |
+
}
|
| 353 |
+
st.session_state.data["habits"].append(new_habit)
|
| 354 |
+
st.session_state.data["completions"][habit_id] = []
|
| 355 |
+
save_data(st.session_state.data)
|
| 356 |
+
st.success(f"Created: {habit_emoji} {habit_name}")
|
| 357 |
+
st.rerun()
|
| 358 |
+
|
| 359 |
+
st.markdown("---")
|
| 360 |
+
|
| 361 |
+
# Export Section
|
| 362 |
+
st.header("π₯ Export Data")
|
| 363 |
+
if st.session_state.data["habits"]:
|
| 364 |
+
csv = export_to_csv()
|
| 365 |
+
st.download_button(
|
| 366 |
+
label="Download CSV",
|
| 367 |
+
data=csv,
|
| 368 |
+
file_name=f"habits_export_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 369 |
+
mime="text/csv",
|
| 370 |
+
use_container_width=True
|
| 371 |
+
)
|
| 372 |
+
st.caption("Export all your habit data for backup or analysis")
|
| 373 |
+
else:
|
| 374 |
+
st.info("Add habits to enable export")
|
| 375 |
+
|
| 376 |
+
st.markdown("---")
|
| 377 |
+
|
| 378 |
+
# Stats Summary
|
| 379 |
+
if st.session_state.data["habits"]:
|
| 380 |
+
st.header("π Quick Stats")
|
| 381 |
+
total_habits = len(st.session_state.data["habits"])
|
| 382 |
+
total_completions = sum(len(v) for v in st.session_state.data["completions"].values())
|
| 383 |
+
active_streaks = sum(
|
| 384 |
+
1 for h in st.session_state.data["habits"]
|
| 385 |
+
if get_streak(h["id"], st.session_state.data["completions"], h) > 0
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
st.metric("Total Habits", total_habits)
|
| 389 |
+
st.metric("Total Completions", total_completions)
|
| 390 |
+
st.metric("Active Streaks", active_streaks)
|
| 391 |
+
|
| 392 |
+
# Display reminders
|
| 393 |
+
if reminders:
|
| 394 |
+
st.markdown("---")
|
| 395 |
+
st.subheader("π Reminders")
|
| 396 |
+
for habit in reminders:
|
| 397 |
+
st.info(f"β° Time to complete: {habit['emoji']} **{habit['name']}**!")
|
| 398 |
+
|
| 399 |
+
# Main Header
|
| 400 |
+
st.markdown('<h1 class="main-header">π₯ Atomic Habits Tracker Pro</h1>', unsafe_allow_html=True)
|
| 401 |
+
|
| 402 |
+
# Daily Quote
|
| 403 |
+
quotes = [
|
| 404 |
+
"You do not rise to the level of your goals. You fall to the level of your systems.",
|
| 405 |
+
"Every action you take is a vote for the type of person you wish to become.",
|
| 406 |
+
"Success is the product of daily habitsβnot once-in-a-lifetime transformations.",
|
| 407 |
+
"The cost of your good habit is in the dozens of moments before you do it.",
|
| 408 |
+
"You should be far more concerned with your current trajectory than with your current results.",
|
| 409 |
+
"Habits are the compound interest of self-improvement.",
|
| 410 |
+
"You don't have to be the victim of your environment. You can also be the architect of it."
|
| 411 |
+
]
|
| 412 |
+
st.markdown(f'<div class="quote-box">"{quotes[datetime.now().day % len(quotes)]}" β James Clear</div>', unsafe_allow_html=True)
|
| 413 |
+
|
| 414 |
+
# Category Filter
|
| 415 |
+
if st.session_state.data["habits"]:
|
| 416 |
+
all_categories = ["All"] + list(CATEGORIES.keys())
|
| 417 |
+
selected_category = st.selectbox("Filter by Category", all_categories, horizontal=True)
|
| 418 |
+
|
| 419 |
+
filtered_habits = st.session_state.data["habits"]
|
| 420 |
+
if selected_category != "All":
|
| 421 |
+
filtered_habits = [h for h in filtered_habits if h["category"] == selected_category]
|
| 422 |
+
|
| 423 |
+
# Main Content
|
| 424 |
+
if not st.session_state.data["habits"]:
|
| 425 |
+
st.info("π Start by adding your first habit in the sidebar! Remember: start small (2-minute rule).")
|
| 426 |
+
else:
|
| 427 |
+
today = datetime.now().strftime("%Y-%m-%d")
|
| 428 |
+
today_display = datetime.now().strftime("%A, %B %d, %Y")
|
| 429 |
+
|
| 430 |
+
st.subheader(f"π
{today_display}")
|
| 431 |
+
|
| 432 |
+
# Group by category if showing all
|
| 433 |
+
if selected_category == "All":
|
| 434 |
+
for category in CATEGORIES.keys():
|
| 435 |
+
category_habits = [h for h in filtered_habits if h["category"] == category]
|
| 436 |
+
if category_habits:
|
| 437 |
+
st.markdown(f"### {CATEGORIES[category]} {category}")
|
| 438 |
+
cols = st.columns(2)
|
| 439 |
+
for idx, habit in enumerate(category_habits):
|
| 440 |
+
with cols[idx % 2]:
|
| 441 |
+
render_habit_card(habit, today)
|
| 442 |
+
else:
|
| 443 |
+
cols = st.columns(2)
|
| 444 |
+
for idx, habit in enumerate(filtered_habits):
|
| 445 |
+
with cols[idx % 2]:
|
| 446 |
+
render_habit_card(habit, today)
|
| 447 |
+
|
| 448 |
+
def render_habit_card(habit, today):
|
| 449 |
+
"""Render individual habit card"""
|
| 450 |
+
habit_id = habit["id"]
|
| 451 |
+
completions = st.session_state.data["completions"].get(habit_id, [])
|
| 452 |
+
is_completed_today = today in completions
|
| 453 |
+
streak = get_streak(habit_id, st.session_state.data["completions"], habit)
|
| 454 |
+
weekly_rate = get_weekly_completion(habit_id, st.session_state.data["completions"])
|
| 455 |
+
monthly_count, monthly_rate = get_monthly_stats(habit_id, st.session_state.data["completions"])
|
| 456 |
+
|
| 457 |
+
with st.container():
|
| 458 |
+
st.markdown(f"""
|
| 459 |
+
<div class="habit-card" style="border-left: 5px solid {habit['color']};">
|
| 460 |
+
""", unsafe_allow_html=True)
|
| 461 |
+
|
| 462 |
+
# Header with category badge
|
| 463 |
+
col1, col2 = st.columns([3, 1])
|
| 464 |
+
|
| 465 |
+
with col1:
|
| 466 |
+
st.markdown(f"### {habit['emoji']} {habit['name']}")
|
| 467 |
+
st.markdown(f'<span class="category-badge">{CATEGORIES[habit["category"]]} {habit["category"]}</span>', unsafe_allow_html=True)
|
| 468 |
+
st.caption(f"β° Reminder at {habit.get('reminder_time', '09:00')}")
|
| 469 |
+
|
| 470 |
+
with col2:
|
| 471 |
+
if is_completed_today:
|
| 472 |
+
st.success("β
Done!")
|
| 473 |
+
if st.button("Undo", key=f"undo_{habit_id}", help="Mark as not done"):
|
| 474 |
+
st.session_state.data["completions"][habit_id].remove(today)
|
| 475 |
+
save_data(st.session_state.data)
|
| 476 |
+
st.rerun()
|
| 477 |
+
else:
|
| 478 |
+
if st.button("Complete β", key=f"complete_{habit_id}", type="primary", use_container_width=True):
|
| 479 |
+
st.session_state.data["completions"][habit_id].append(today)
|
| 480 |
+
save_data(st.session_state.data)
|
| 481 |
+
st.balloons()
|
| 482 |
+
st.rerun()
|
| 483 |
+
|
| 484 |
+
# Stats row
|
| 485 |
+
stat_col1, stat_col2, stat_col3 = st.columns(3)
|
| 486 |
+
|
| 487 |
+
with stat_col1:
|
| 488 |
+
if streak > 0:
|
| 489 |
+
st.markdown(f'<span class="streak-badge">π₯ {streak} day streak</span>', unsafe_allow_html=True)
|
| 490 |
+
else:
|
| 491 |
+
st.markdown("πΉ Start today!")
|
| 492 |
+
|
| 493 |
+
with stat_col2:
|
| 494 |
+
st.progress(weekly_rate / 100, text=f"Week: {weekly_rate:.0f}%")
|
| 495 |
+
|
| 496 |
+
with stat_col3:
|
| 497 |
+
st.caption(f"30d: {monthly_count} days ({monthly_rate:.0f}%)")
|
| 498 |
+
|
| 499 |
+
# Edit/Delete
|
| 500 |
+
with st.expander("βοΈ Settings"):
|
| 501 |
+
edit_col1, edit_col2 = st.columns(2)
|
| 502 |
+
|
| 503 |
+
with edit_col1:
|
| 504 |
+
new_time = st.time_input("Update reminder",
|
| 505 |
+
value=datetime.strptime(habit.get("reminder_time", "09:00"), "%H:%M").time(),
|
| 506 |
+
key=f"time_{habit_id}")
|
| 507 |
+
if new_time.strftime("%H:%M") != habit.get("reminder_time", "09:00"):
|
| 508 |
+
habit["reminder_time"] = new_time.strftime("%H:%M")
|
| 509 |
+
save_data(st.session_state.data)
|
| 510 |
+
st.success("Updated!")
|
| 511 |
+
|
| 512 |
+
with edit_col2:
|
| 513 |
+
if st.button("ποΈ Delete Habit", key=f"del_{habit_id}", type="secondary"):
|
| 514 |
+
st.session_state.data["habits"].remove(habit)
|
| 515 |
+
del st.session_state.data["completions"][habit_id]
|
| 516 |
+
save_data(st.session_state.data)
|
| 517 |
+
st.rerun()
|
| 518 |
+
|
| 519 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 520 |
+
|
| 521 |
+
# Analytics Section
|
| 522 |
+
st.markdown("---")
|
| 523 |
+
st.subheader("π Analytics Dashboard")
|
| 524 |
+
|
| 525 |
+
if st.session_state.data["habits"]:
|
| 526 |
+
# Category breakdown
|
| 527 |
+
cat_data = {}
|
| 528 |
+
for habit in st.session_state.data["habits"]:
|
| 529 |
+
cat = habit["category"]
|
| 530 |
+
completions = len(st.session_state.data["completions"].get(habit["id"], []))
|
| 531 |
+
cat_data[cat] = cat_data.get(cat, 0) + completions
|
| 532 |
+
|
| 533 |
+
fig_pie = go.Figure(data=[go.Pie(
|
| 534 |
+
labels=[f"{CATEGORIES[k]} {k}" for k in cat_data.keys()],
|
| 535 |
+
values=list(cat_data.values()),
|
| 536 |
+
hole=.4,
|
| 537 |
+
marker_colors=px.colors.qualitative.Set3
|
| 538 |
+
)])
|
| 539 |
+
fig_pie.update_layout(title_text="Completions by Category", showlegend=True)
|
| 540 |
+
|
| 541 |
+
# Timeline data
|
| 542 |
+
timeline_data = defaultdict(int)
|
| 543 |
+
for habit_id, dates in st.session_state.data["completions"].items():
|
| 544 |
+
for date in dates:
|
| 545 |
+
timeline_data[date] += 1
|
| 546 |
+
|
| 547 |
+
if timeline_data:
|
| 548 |
+
df_timeline = pd.DataFrame([
|
| 549 |
+
{"Date": datetime.strptime(d, "%Y-%m-%d"), "Total Completions": c}
|
| 550 |
+
for d, c in sorted(timeline_data.items())
|
| 551 |
+
])
|
| 552 |
+
|
| 553 |
+
fig_timeline = px.line(df_timeline, x="Date", y="Total Completions",
|
| 554 |
+
title="Your Consistency Over Time",
|
| 555 |
+
markers=True)
|
| 556 |
+
fig_timeline.update_layout(xaxis_title="Date", yaxis_title="Habits Completed")
|
| 557 |
+
else:
|
| 558 |
+
fig_timeline = go.Figure()
|
| 559 |
+
fig_timeline.update_layout(title="Start completing habits to see your timeline!")
|
| 560 |
+
|
| 561 |
+
col1, col2 = st.columns(2)
|
| 562 |
+
with col1:
|
| 563 |
+
st.plotly_chart(fig_pie, use_container_width=True)
|
| 564 |
+
with col2:
|
| 565 |
+
st.plotly_chart(fig_timeline, use_container_width=True)
|
| 566 |
+
|
| 567 |
+
# Detailed table
|
| 568 |
+
st.subheader("π Habit Performance Table")
|
| 569 |
+
table_data = []
|
| 570 |
+
for habit in st.session_state.data["habits"]:
|
| 571 |
+
habit_id = habit["id"]
|
| 572 |
+
completions = st.session_state.data["completions"].get(habit_id, [])
|
| 573 |
+
streak = get_streak(habit_id, st.session_state.data["completions"], habit)
|
| 574 |
+
total = len(completions)
|
| 575 |
+
|
| 576 |
+
table_data.append({
|
| 577 |
+
"Habit": f"{habit['emoji']} {habit['name']}",
|
| 578 |
+
"Category": f"{CATEGORIES[habit['category']]} {habit['category']}",
|
| 579 |
+
"Current Streak": f"π₯ {streak}" if streak > 0 else "0",
|
| 580 |
+
"Total Days": total,
|
| 581 |
+
"Success Rate": f"{(total / max(1, (datetime.now() - datetime.strptime(habit['created_at'], '%Y-%m-%d')).days)) * 100:.1f}%"
|
| 582 |
+
})
|
| 583 |
+
|
| 584 |
+
df_table = pd.DataFrame(table_data)
|
| 585 |
+
st.dataframe(df_table, use_container_width=True, hide_index=True)
|
| 586 |
+
|
| 587 |
+
# Calendar heatmap for selected habit
|
| 588 |
+
st.subheader("π
Detailed Activity View")
|
| 589 |
+
selected_habit = st.selectbox(
|
| 590 |
+
"Select habit to view calendar",
|
| 591 |
+
options=[h["name"] for h in st.session_state.data["habits"]],
|
| 592 |
+
format_func=lambda x: next(f"{h['emoji']} {h['name']} ({h['category']})" for h in st.session_state.data["habits"] if h["name"] == x)
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
selected_habit_data = next(h for h in st.session_state.data["habits"] if h["name"] == selected_habit)
|
| 596 |
+
completions = st.session_state.data["completions"].get(selected_habit_data["id"], [])
|
| 597 |
+
|
| 598 |
+
if completions:
|
| 599 |
+
# Create comprehensive calendar view
|
| 600 |
+
dates_data = []
|
| 601 |
+
for i in range(90): # Last 90 days
|
| 602 |
+
date = datetime.now().date() - timedelta(days=i)
|
| 603 |
+
date_str = date.strftime("%Y-%m-%d")
|
| 604 |
+
completed = date_str in completions
|
| 605 |
+
dates_data.append({
|
| 606 |
+
"Date": date,
|
| 607 |
+
"Completed": "Yes" if completed else "No",
|
| 608 |
+
"Week": date.isocalendar()[1],
|
| 609 |
+
"Day": date.strftime("%a"),
|
| 610 |
+
"Month": date.strftime("%b")
|
| 611 |
+
})
|
| 612 |
+
|
| 613 |
+
cal_df = pd.DataFrame(dates_data)
|
| 614 |
+
|
| 615 |
+
# Heatmap
|
| 616 |
+
fig_cal = px.density_heatmap(
|
| 617 |
+
cal_df,
|
| 618 |
+
x="Week",
|
| 619 |
+
y="Day",
|
| 620 |
+
z=[1 if c == "Yes" else 0 for c in cal_df["Completed"]],
|
| 621 |
+
title=f"Last 90 Days Activity - {selected_habit}",
|
| 622 |
+
color_continuous_scale=[(0, "#e0e0e0"), (1, selected_habit_data["color"])],
|
| 623 |
+
labels={"color": "Completed"}
|
| 624 |
+
)
|
| 625 |
+
st.plotly_chart(fig_cal, use_container_width=True)
|
| 626 |
+
else:
|
| 627 |
+
st.info("No data yet. Complete your habit today to start tracking!")
|
| 628 |
+
|
| 629 |
+
# Footer
|
| 630 |
+
st.markdown("---")
|
| 631 |
+
st.markdown(f"""
|
| 632 |
+
<div style="text-align: center; padding: 20px;">
|
| 633 |
+
<p class="footer-text">Built with β€οΈ using Streamlit | Deployed on Hugging Face Spaces</p>
|
| 634 |
+
<p class="footer-text" style="font-size: 0.8rem;">"Small changes, remarkable results" β James Clear</p>
|
| 635 |
+
<p class="footer-text" style="font-size: 0.7rem;">Current theme: {"Dark" if st.session_state.dark_mode else "Light"} Mode</p>
|
| 636 |
+
</div>
|
| 637 |
+
""", unsafe_allow_html=True)
|