Compare commits

...

135 Commits

Author SHA1 Message Date
Yapollon
5251a80d02 Merge branch 'master' of https://repo.cimeyclust.com/CimeyClust/HabitTracker 2024-03-25 12:41:12 +01:00
Yapollon
f53b3c4059 Fix Ups
some changes i had on my local folder
2024-03-25 12:41:01 +01:00
Verox
ff670abc1d Fixed redirect after use removal from habit list 2024-03-14 20:09:29 +01:00
Verox
fce1593e32 Fixed user ownership check if habit_lists 2024-03-14 19:50:50 +01:00
Yapollon
e57cc58b3e new icon! 2024-03-13 12:04:00 +01:00
Yapollon
94a84ecfe4 Merge branch 'master' of https://repo.cimeyclust.com/CimeyClust/HabitTracker 2024-03-12 11:14:44 +01:00
Yapollon
d2730a58fe insane fix 2024-03-12 11:14:28 +01:00
Verox001
2b8dba9c0b Finished and beautified ER diagram 2024-03-12 10:11:01 +01:00
Yapollon
273eca9da2 fixed heatmap 2024-03-12 07:54:00 +01:00
Verox
88fc1ef573 Added habit list being fixed at one after page reload 2024-03-11 19:07:50 +01:00
Yapollon
1317161ae9 HabitList Drag and Drop
It works now, yay!
2024-03-10 22:02:57 +01:00
Yapollon
03e1f22a87 Cleanup 2024-03-10 21:02:33 +01:00
Yapollon
fdc90fe118 Slot System for HabitLists 2024-03-10 13:10:54 +01:00
Yapollon
f81ec8368c Progessbar Update
If the Time selected by the unit is over the count will drop to 0
2024-03-09 15:15:07 +01:00
Yapollon
fe91faf5b1 dynamic Time 2024-03-09 14:48:07 +01:00
Yapollon
9d945f5edc Profile Image Optimizations
made it as fast as i could, maybe there are faster solutions but I'm fine with this
2024-03-09 14:24:02 +01:00
Yapollon
be16821be2 Delete Account
You can now delete the account in the profile settings
Also profile images associated with the user will be removed from the file system

+ fixed that HabitList deletion doesn't cause Habit Deletion
2024-03-08 15:49:46 +01:00
Yapollon
0d2baf6325 heatmap.css 2024-03-08 14:05:22 +01:00
Yapollon
1d88041023 Reformating 2 2024-03-08 12:51:24 +01:00
Yapollon
51509bb04f Reformating
Nothing got changed
2024-03-08 12:48:19 +01:00
Yapollon
07a204a3ed Optimized Heatmap Color 2024-03-08 12:35:46 +01:00
Yapollon
09a2a6ac5f Merge remote-tracking branch 'origin/master' 2024-03-07 20:43:15 +01:00
Yapollon
8947c6d54c MIGRATE: Implemented user color setting. 2024-03-07 20:43:04 +01:00
Verox001
d465287316 Finished habit_list invitation 2024-03-07 20:34:30 +01:00
Yapollon
ce68bc9786 Merge branch 'master' of https://repo.cimeyclust.com/CimeyClust/HabitTracker 2024-03-07 20:07:15 +01:00
Yapollon
383db050c2 Color Picker Update 2 2024-03-07 20:07:05 +01:00
343bb26536 favicon.ico 2024-03-07 19:58:05 +01:00
a46b464ca6 Merge remote-tracking branch 'origin/master' 2024-03-07 19:22:48 +01:00
b02458d96c User Stack bei beigetretner Liste 2024-03-07 19:22:32 +01:00
Verox001
ece973ad28 Fixed checked habit sorting 2024-03-07 19:01:00 +01:00
Yapollon
8094bd7865 Merge remote-tracking branch 'origin/master' 2024-03-07 18:48:30 +01:00
Yapollon
1d8dd83086 MIGRATE: Implemented user color setting. 2024-03-07 18:48:19 +01:00
Luis
e86c7ac941 Merge remote-tracking branch 'origin/master' 2024-03-07 18:46:14 +01:00
Luis
5f75997617 moved accept_list from habitListe to User 2024-03-07 18:44:13 +01:00
412666705c Liste verlassen feature 2024-03-07 18:39:15 +01:00
Luis
69b3c257d1 Merge remote-tracking branch 'origin/master' 2024-03-07 18:28:39 +01:00
Luis
db45e7f059 added a way to accept shared habit_lists instead of directly accepting. accepted via habit_list.accept_List(habit_List.id, user.id) 2024-03-07 18:28:06 +01:00
Yapollon
45f23c88e2 MIGRATE: Implemented user color setting. 2024-03-07 17:54:49 +01:00
164534ada1 Merge remote-tracking branch 'origin/master' 2024-03-07 17:54:11 +01:00
20f9800203 edit users 2024-03-07 17:54:06 +01:00
Luis
0dbac5afd5 added accepted column. 2024-03-07 17:47:09 +01:00
Luis
817ef7f683 Merge remote-tracking branch 'origin/master' 2024-03-07 17:09:22 +01:00
Luis
6e84e6aeaf Added Button for Habit suggestions. 2024-03-07 17:05:18 +01:00
Yapollon
66aabf697c Colorpicker Update 1
we live, we love, we want the colorpicker
2024-03-07 16:26:56 +01:00
Yapollon
5a68a2ef39 Update app.py 2024-03-07 16:00:04 +01:00
d340a1e19f Merge remote-tracking branch 'origin/master'
# Conflicts:
#	templates/components/habit_lists.html
2024-03-07 15:57:10 +01:00
0312bc10dc heatmap current day marked 2024-03-07 15:56:33 +01:00
Yapollon
11104fe96b cleaned code
yippie
2024-03-07 15:56:06 +01:00
Yapollon
ca9168cff4 better design 2024-03-07 15:48:39 +01:00
Yapollon
e3ea8d7b17 Update get_heatmap 2024-03-07 15:07:26 +01:00
Yapollon
82b36bcd0b Heatmap fix 2024-03-07 15:04:29 +01:00
Yapollon
56af47bda3 Merge branch 'master' of https://repo.cimeyclust.com/CimeyClust/HabitTracker 2024-03-07 14:28:25 +01:00
Yapollon
5e71260dbe Responsive Heatmap!
- Deleted the shitty spline model
2024-03-07 14:28:20 +01:00
32cec2e731 times errors and cards 2024-03-07 14:20:45 +01:00
Yapollon
22ecf59c62 Gif Update
We going to Fazbears Pizza with this one
2024-03-07 10:38:53 +01:00
Yapollon
def7e1845e very important fix 2024-03-06 13:07:49 +01:00
Yapollon
67ef6296c9 Update SQLiteClient.py 2024-03-06 11:14:40 +01:00
Yapollon
17a3f7bacc Merge branch 'master' of https://repo.cimeyclust.com/CimeyClust/HabitTracker 2024-03-06 11:14:27 +01:00
Yapollon
adeab273a6 Update User.py 2024-03-06 11:14:15 +01:00
Verox001
66d8eb8f78 Improved responsive design 2024-03-06 11:09:59 +01:00
Yapollon
776eaec21b Merge remote-tracking branch 'origin/master' 2024-03-06 11:05:16 +01:00
Verox001
4dc494e67a Implemented habit list deletion 2024-03-06 11:04:32 +01:00
Yapollon
e9368f567e HabitList fix 2024-03-06 11:04:15 +01:00
Verox001
b04068da4e Improved layouting 2024-03-06 10:52:47 +01:00
af86caa196 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	templates/index.html
2024-03-06 10:33:04 +01:00
30edcec39c Merge remote-tracking branch 'origin/master' 2024-03-05 11:16:03 +01:00
424a423f7c delete list 2024-03-05 11:15:43 +01:00
Verox001
785edd1f88 Fixed SQL operational error 2024-03-05 10:54:26 +01:00
ec82160214 Merge remote-tracking branch 'origin/master' 2024-03-05 10:53:46 +01:00
Verox001
51742411ca Fixed day count 2024-03-05 10:49:29 +01:00
Verox001
295f02509c Started app at 0.0.0.0 host 2024-03-05 10:37:18 +01:00
Verox001
7aec15c4cd Updated requirements.txt 2024-03-05 10:33:28 +01:00
d26b1a4ff8 Merge remote-tracking branch 'origin/master' 2024-03-05 10:27:33 +01:00
Verox001
8814a6a834 Added requirements.txt 2024-03-05 10:13:50 +01:00
c258dec2bd Spline Integration 2024-03-05 10:13:02 +01:00
45f3974514 fixed yassins wrongdoing 2024-03-05 10:05:38 +01:00
Verox001
6282c374f7 Fixed habit times check 2024-03-05 09:59:09 +01:00
4e996180fe Merge remote-tracking branch 'origin/master' 2024-03-05 09:50:13 +01:00
Verox001
53a884d23e Fixed user None Check 2024-03-05 09:49:38 +01:00
0208ef5f47 Merge remote-tracking branch 'origin/master' 2024-03-05 09:48:28 +01:00
Verox001
f1c4d26b0a Merge remote-tracking branch 'origin/master' 2024-03-05 09:47:41 +01:00
Verox001
2a249a32e5 Fixed user None Check 2024-03-05 09:47:23 +01:00
bb1cfe77f2 Merge remote-tracking branch 'origin/master' 2024-03-05 09:45:57 +01:00
3c388b03a7 Merge remote-tracking branch 'origin/master' 2024-03-05 09:45:23 +01:00
1777ee28af 120 Flammen 2024-03-05 09:45:14 +01:00
Verox001
6337eaff81 Removed habittracker content when not logged in 2024-03-05 09:44:54 +01:00
3427e8f271 Merge remote-tracking branch 'origin/master' 2024-03-05 09:44:12 +01:00
Verox001
9fdbef1b7d Verify email is not used already on signup 2024-03-05 09:43:13 +01:00
5bc8b1dc26 uniform design (card) + names + deleted Home 2024-03-01 10:31:24 +01:00
0b927009b5 uniform design (card) + names + deleted Home 2024-03-01 10:30:41 +01:00
7ac9168147 Merge remote-tracking branch 'origin/master' 2024-03-01 09:26:54 +01:00
4bce1aab30 uniform design (card) 2024-03-01 09:26:45 +01:00
Verox001
c2c38f55c4 Remove plus icon from not primary users 2024-03-01 09:25:36 +01:00
f7967c2dfd many 2024-03-01 09:25:27 +01:00
Verox001
2dc3cbf047 Implemented User Display on Habitlist 2024-03-01 09:17:52 +01:00
Verox001
b6a9bf5520 Implemented User-Invite Page 2024-03-01 08:50:33 +01:00
7771c1eea2 edit page 2024-03-01 08:19:53 +01:00
Yapollon
e20defa4e1 Merge branch 'master' of https://repo.cimeyclust.com/CimeyClust/HabitTracker 2024-03-01 08:14:20 +01:00
Yapollon
1b665b8be0 Improved user addition 2024-03-01 08:13:58 +01:00
Verox001
afb57ef077 Fixed minor bugs 2024-03-01 08:06:00 +01:00
Yapollon
a8a3382f15 Habit Streak Update++ 2024-03-01 07:53:47 +01:00
Yapollon
fc570208bf Habit Streak Update (2/2)+
Minor change
2024-02-28 14:28:31 +01:00
Yapollon
8f57c2d9e3 Merge branch 'master' of https://repo.cimeyclust.com/CimeyClust/HabitTracker 2024-02-28 14:14:20 +01:00
Yapollon
e2f1402d50 Habit Streak Update (2/2)
Yeah reworked the entire thing so that the checked, count and streak value is stored in the Habit Table
2024-02-28 14:14:10 +01:00
Verox001
2a6aa0b04f Added list_attribute addition 2024-02-28 11:18:14 +01:00
Verox001
7e9d445051 Fixed multiple list sorting 2024-02-28 10:57:48 +01:00
d0c5127c92 Merge remote-tracking branch 'origin/master' 2024-02-28 10:48:29 +01:00
ba8bc06d89 inprovements 2024-02-28 10:46:26 +01:00
Yapollon
2f7541aa55 Habit Streak Update (1/2)
first part works autonomous except the uncheck
2024-02-28 10:36:04 +01:00
9e1bd7b65e better looking html code and fixed a bug 2024-02-20 20:55:06 +01:00
0f35a67391 tabs layouts 2024-02-20 11:17:06 +01:00
Yapollon
bcfb7aaec6 Update Habit.py
Streak for Daylies was added.
2024-02-20 10:57:45 +01:00
Verox001
5138e6d25f Fixed some imports 2024-02-20 10:13:42 +01:00
Verox001
4711106653 Added get streak method again 2024-02-20 10:13:00 +01:00
Verox001
6a46104efa Fixed merge error 2024-02-20 10:12:20 +01:00
cb1991e2cc Merge remote-tracking branch 'origin/master'
# Conflicts:
#	models/Habit.py
2024-02-20 10:05:07 +01:00
7ed5dfbf58 Flammen und Namen 2024-02-20 10:02:52 +01:00
0e798adb89 Flammen und Namen 2024-02-20 10:02:26 +01:00
bbd6f27af1 Flammen und Namen 2024-02-20 10:01:36 +01:00
Verox001
f07456a95b Reduced weekday count by one 2024-02-20 10:01:08 +01:00
08ff2cfc16 Fixed bug 2024-02-20 10:00:31 +01:00
Verox001
94fa2f567f Reverted merge commit from model 2024-02-20 09:58:34 +01:00
Yapollon
f0275108a5 Merge branch 'model' 2024-02-20 09:52:31 +01:00
92b099b112 card Tabs
(cherry picked from commit 5c0a2b588644b4d8ad6148c2582a551d770f4261)
2024-02-20 09:49:12 +01:00
d3013a5982 card Tabs
# Conflicts:
#	templates/index.html
2024-02-20 09:48:14 +01:00
Verox
8b9340e867 Merge remote-tracking branch 'origin/model'
# Conflicts:
#	db/SQLiteClient.py
#	models/Habit.py
#	models/HabitList.py
#	models/HabitTrackings.py
#	models/User.py
2024-02-19 20:14:55 +01:00
Verox001
219c080bf5 Merge remote-tracking branch 'origin/luis' 2024-02-16 09:41:08 +01:00
a74e8b0cf1 Flammen und Namen 2024-02-16 09:28:00 +01:00
5c0a2b5886 card Tabs 2024-02-16 09:25:31 +01:00
2e790d9cf8 Merge remote-tracking branch 'origin/master' 2024-02-16 08:31:27 +01:00
Verox001
d1a1f6fb0d Reverted merge commit from model 2024-02-16 08:28:47 +01:00
ce0a6b588b gap for spacing and heatmap but unfinished 2024-02-14 11:15:37 +01:00
6143f44442 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	templates/index.html
2024-02-14 10:59:24 +01:00
454ce33846 Merge remote-tracking branch 'origin/master' 2024-02-14 10:36:49 +01:00
3a86eb3802 spacing udn cards 2024-02-13 11:15:19 +01:00
34 changed files with 1950 additions and 725 deletions

BIN
ER.dia

Binary file not shown.

BIN
ER.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 97 KiB

BIN
ER_transparent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

682
app.py
View File

@ -1,7 +1,9 @@
import datetime
import hashlib
import os
from PIL import Image
from PIL import Image, ImageSequence
import concurrent.futures
from flask import Flask, render_template, redirect, url_for, request, jsonify
from flask_login import login_required, LoginManager, login_user, logout_user, current_user
@ -15,13 +17,14 @@ from utils import anonymous_required
# Create a new Flask instance
app = Flask(__name__)
app.secret_key = 'PSSSSSHHHT!'
UPLOAD_FOLDER = 'static/profile_images/' # Folder to store profile images
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# Initialize the Flask-Login extension
login_manager = LoginManager()
login_manager.login_view = 'login'
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return User.get(user_id)
@ -32,18 +35,73 @@ def inject_user():
return dict(user=current_user)
@app.context_processor
def inject_notifications():
if current_user.is_authenticated:
habit_lists = current_user.get_unaccepted_habitLists()
lists = []
for habit_list in habit_lists:
# Check if the user is the first user in the list
if habit_list.get_users()[0].id != current_user.id:
lists.append(habit_list)
return dict(notifications=lists)
return dict(notifications=[])
# Create a new route
@app.route('/')
def index():
if current_user.is_authenticated:
habit_lists = current_user.get_habitLists()
name = "Hallo " + current_user.name
heatmap_values, day = current_user.get_heatmap()
heatmap_color = current_user.heatmap_color
else:
habit_lists = []
name = "Bitte melde dich an."
heatmap_values = []
day = None
heatmap_color = None
# Sort habit_lists based on their order attribute
habit_lists = sorted(habit_lists, key=lambda habitList: habitList.slot)
# Sort habits within each habit_list by slot
for habit_list in habit_lists:
habit_list.habits = sorted(habit_list.get_habits(), key=lambda habit: (habit.checked, habit.slot))
for habit in habit_list.get_habits():
habit.load_statistics()
# Get active_list from query parameter
try:
active_list = int(request.args.get('list'))
except (ValueError, TypeError):
active_list = None
return render_template(
'index.html',
title=name,
habit_lists=habit_lists,
heatmap_values=heatmap_values,
day=day,
color=heatmap_color,
errors={},
active_list=active_list
)
###################### Login & Signup #####################
@app.route('/login')
@anonymous_required
def login():
return render_template('auth/login.html', errors={})
@app.route('/signup')
@anonymous_required
def signup():
return render_template('auth/signup.html', errors={})
@app.route('/login', methods=['POST'])
def login_post():
email = request.form.get('email')
@ -77,7 +135,6 @@ def login_post():
# Redirect to login page
return redirect(url_for('index'))
@app.route('/signup', methods=['POST'])
def signup_post():
email = request.form.get('email')
@ -93,6 +150,10 @@ def signup_post():
if not password:
errors['password'] = 'Das Passwort ist erforderlich.'
# Check if email is already in use
if User.get_by_email(email):
errors['email'] = 'E-Mail Adresse bereits in Benutzung.'
if errors:
return render_template(
'auth/signup.html',
@ -109,7 +170,6 @@ def signup_post():
# Redirect to login page
return redirect(url_for('index'))
@app.route('/logout')
@login_required
def logout():
@ -117,31 +177,11 @@ def logout():
logout_user()
return redirect(url_for('index'))
###########################################################
# Create a new route
@app.route('/')
def index():
if current_user.is_authenticated:
habit_lists = current_user.get_habitLists()
name = "Hallo " + current_user.name
else:
habit_lists = []
name = "Bitte melde dich an."
# Sort habits by whether they have been checked today and then by slot
for habit_list in habit_lists:
habit_list.habits = sorted(habit_list.get_habits(), key=lambda habit: (not habit.checked, habit.slot))
return render_template(
'index.html',
title=name,
utc_dt=datetime.datetime.now().strftime("%d.%m.%Y %H:%M %A"),
habit_lists=habit_lists,
errors={},
)
########################## Habit ##########################
@app.route('/habit')
@login_required
def habit_creation():
@ -152,7 +192,6 @@ def habit_creation():
errors={},
)
@app.route('/habit', methods=['POST'])
@login_required
def habit_create():
@ -195,6 +234,16 @@ def habit_create():
except ValueError:
errors['list_query'] = 'Die Anzahl muss eine Zahl sein.'
# Check if times is possible to achieve
if unit == 'Tag' and times != 1:
errors['times'] = 'Die Anzahl muss 1 sein, wenn das Habit täglich ist.'
if unit == 'Woche' and times > 7:
errors['times'] = 'Die Anzahl darf höchstens 7 sein, wenn das Habit wöchentlich ist.'
if unit == 'Monat' and times > 31:
errors['times'] = 'Die Anzahl darf höchstens 31 sein, wenn das Habit monatlich ist.'
if unit == 'Jahr' and times > 365:
errors['times'] = 'Die Anzahl darf höchstens 365 sein, wenn das Habit jährlich ist.'
if errors:
return render_template(
'habit.html',
@ -220,12 +269,201 @@ def habit_create():
unit = 1
# Save habit to database
habit = Habit.create(list_id, name, times, note, unit)
Habit.create(list_id, name, times, note, unit)
# Back to index
return redirect(url_for('index'))
@app.route('/edit-habit')
@login_required
def edit_habit():
habit_id = int(request.args.get("habit"))
habit = Habit.get(habit_id)
units = ["Tag", "Woche", "Monat", "Jahr"]
return render_template(
"edit-habit.html",
title=habit.name,
habit=habit.id,
name=habit.name,
note=habit.note,
times=habit.times,
unit=units[habit.unit],
errors={}
)
@app.route('/edit-habit', methods=['POST'])
@login_required
def edit_habit_change():
units = ["Tag", "Woche", "Monat", "Jahr"]
name = request.form.get('name')
note = request.form.get('note')
times = request.form.get('times')
unit = request.form.get('unit')
list_id = request.form.get('habit')
habit = Habit.get(list_id)
# Check for errors
errors = {}
if not name:
errors['name'] = 'Der Name ist erforderlich.'
if not times:
errors['times'] = 'Die Anzahl ist erforderlich.'
if not note:
note = ''
if not unit:
errors['unit'] = 'Die Einheit ist erforderlich.'
if not list_id:
errors['habit'] = 'Das Habit ist erforderlich.'
# Check if times is an integer
try:
print(times)
times = int(times)
# Check that times is greater than 0
if times <= 0:
errors['times'] = 'Die Anzahl muss größer als 0 sein.'
except ValueError:
errors['times'] = 'Die Anzahl muss eine Zahl sein.'
# Check that unit is valid
if unit not in ['Tag', 'Woche', 'Monat', 'Jahr']:
errors['unit'] = 'Die Einheit ist ungültig.'
# check if list_id is an int
try:
int(list_id)
except ValueError:
errors['list_query'] = 'Die Anzahl muss eine Zahl sein.'
# Check if times is possible to achieve
if unit == 'Tag' and times != 1:
errors['times'] = 'Die Anzahl muss 1 sein, wenn das Habit täglich ist.'
if unit == 'Woche' and times > 7:
errors['times'] = 'Die Anzahl darf höchstens 7 sein, wenn das Habit wöchentlich ist.'
if unit == 'Monat' and times > 31:
errors['times'] = 'Die Anzahl darf höchstens 31 sein, wenn das Habit monatlich ist.'
if unit == 'Jahr' and times > 365:
errors['times'] = 'Die Anzahl darf höchstens 365 sein, wenn das Habit jährlich ist.'
if errors:
return render_template(
"edit-habit.html",
title=habit.name,
habit=habit.id,
name=habit.name,
note=habit.note,
times=habit.times,
unit=units[habit.unit],
errors=errors
)
# Map unit to integer
if unit == 'Tag':
unit = 0
elif unit == 'Woche':
unit = 1
elif unit == 'Monat':
unit = 2
elif unit == 'Jahr':
unit = 3
else:
unit = 1
# Save habit to database
print(name, note, times, unit)
habit.name, habit.note, habit.times, habit.unit = name, note, times, unit
habit.update()
# Back to index
return redirect(url_for('index'))
@app.route('/check', methods=['POST'])
@login_required
def check_habit():
habit_id = request.get_json()["habitId"]
habit = Habit.get(habit_id)
if habit is None:
return {"error": "Habit not found"}
# Check if habit belongs to user
users = habit.habit_list().get_users()
if current_user not in users:
return {"error": "Habit does not belong to user"}
trackings = habit.get_habitTrackings()
# Check if habit has been tracked today
delete_tracking = None
for tracking in trackings:
if tracking.created_at.date() == datetime.date.today():
delete_tracking = tracking
if not delete_tracking:
HabitTracking.create(habit_id)
habit.fill_statistics()
else:
delete_tracking.delete()
habit.reset_statistics()
habit.load_statistics()
heatmap_values, day = current_user.get_heatmap()
return {
"habitId": habit_id,
"unchecked": not delete_tracking,
"percentage": habit.percentage,
"streak": habit.streak,
"heatmap": heatmap_values,
"day": day,
}
@app.route('/delete', methods=['POST'])
@login_required
def delete_habit():
habit_id = request.get_json()["habitId"]
habit = Habit.get(habit_id)
if habit is None:
return {"error": "Habit not found"}
# Check if habit belongs to user
if current_user not in habit.habit_list().get_users():
return {"error": "Habit does not belong to user"}
habit.delete()
return {}
@app.route('/reorder-habit', methods=['POST'])
@login_required
def reorder_habits():
new_index = request.get_json()["newIndex"] + 1
habit = Habit.get(request.get_json()["habitId"])
if habit is None:
return {"error": "Habit not found"}
# Check if habit belongs to user
users = habit.habit_list().get_users()
if current_user not in users:
return {"error": "Habit does not belong to user"}
habit.update_slot(new_index)
return {}
###########################################################
######################## HabitList ########################
@app.route('/habit-list')
@login_required
def habit_list_creation():
@ -235,7 +473,6 @@ def habit_list_creation():
errors={},
)
@app.route('/habit-list', methods=['POST'])
@login_required
def habit_list_create():
@ -247,7 +484,7 @@ def habit_list_create():
if not name:
errors['name'] = 'Der Name ist erforderlich.'
if not description:
note = ''
description = ''
if errors:
return render_template(
@ -259,12 +496,236 @@ def habit_list_create():
)
# Save habit to database
habit = HabitList.create(current_user.id, name, description)
HabitList.create(current_user.id, name, description)
# Back to index
return redirect(url_for('index'))
@app.route('/delete-list', methods=['POST'])
@login_required
def delete_list():
list_id = request.get_json()["listId"]
habit_list = HabitList.get(list_id)
if habit_list is None:
return {"error": "List not found"}
# Check if habit belongs to user
if current_user not in habit_list.get_users():
return {"error": "List does not belong to user"}
habit_list.delete(current_user.id)
return {}
@app.route('/users')
@login_required
def users():
habit_list_id = request.args.get('habit_list', current_user.get_habitLists()[0].id)
habit_list = HabitList.get(int(habit_list_id))
users = habit_list.get_users()
# Remove the current user from the list
users = [user for user in users if user.id != current_user.id]
return render_template(
'users.html',
title='Teilnehmer',
habit_list=habit_list,
users=users,
errors={},
)
@app.route('/users', methods=['POST'])
@login_required
def add_user():
email = request.form.get('email')
habit_list_id = request.form.get('habit_list_id')
habit_list = HabitList.get(int(habit_list_id))
# Check for errors
errors = {}
if not email:
errors['email'] = 'Die E-Mail Adresse ist erforderlich.'
if not habit_list_id:
errors['habit_list'] = 'Die Habitliste ist erforderlich.'
if errors:
return render_template(
'users.html',
title='Teilnehmer',
email=email,
habit_list=habit_list,
errors=errors,
users=habit_list.get_users(),
)
# Check if user exists
user = User.get_by_email(email)
if not user:
errors['email'] = 'E-Mail Adresse nicht gefunden.'
if user and user.id == current_user.id:
errors['email'] = 'Du kannst dich nicht selbst hinzufügen.'
# Check if user is already in the habit list
already = False
if user:
for u in habit_list.get_users():
if u.id == user.id:
already = True
break
if already:
errors['email'] = 'Teilnehmer ist bereits in der Liste.'
if errors:
return render_template(
'users.html',
title='Teilnehmer',
email=email,
habit_list=habit_list,
errors=errors,
users=habit_list.get_users(),
)
# Add user to habit list
habit_list = HabitList.get(int(habit_list_id))
habit_list.add_user(user)
return redirect(url_for('index', habit_list=habit_list.id))
@app.route('/users-edit')
@login_required
def edit_users():
habit_list_id = request.args.get('habit_list', current_user.get_habitLists()[0].id)
habit_list = HabitList.get(int(habit_list_id))
users = habit_list.get_users()
# Remove the current user from the list
users = [user for user in users if user.id != current_user.id]
return render_template(
'users-edit.html',
title='Teilnehmer bearbeiten',
habit_list=habit_list,
users=users,
errors={},
)
@app.route('/user-delete', methods=['POST'])
@login_required
def delete_user_from_list():
habit_list_id = request.form.get('habit_list_id')
habit_list = HabitList.get(int(habit_list_id))
habit_user_id = request.form.get('habit_user_id')
users = habit_list.get_users()
# Remove the current user from the list
users = [user for user in users if user.id != current_user.id]
# Check for errors
errors = {}
if not habit_list_id:
errors['habit_list'] = 'Die Habitliste ist erforderlich.'
if not habit_list_id:
errors['habit_user'] = 'Ein User ist erforderlich.'
if errors:
return render_template(
'users-edit.html',
title='Teilnehmer bearbeiten',
habit_list=habit_list,
users=users,
errors={},
)
# delete user from habit list
id = int(habit_user_id)
habit_list.delete(id)
return redirect(url_for('index', habit_list=habit_list.id))
@app.route('/users-leave')
@login_required
def user_leave():
list_id = request.args.get('habit_list')
habit_list = HabitList.get(list_id)
if habit_list is None:
return {"error": "List not found"}
# Check if habit belongs to user
if current_user not in habit_list.get_users():
return {"error": "List does not belong to user"}
habit_list.remove_user(current_user.id)
return redirect(url_for("index"))
@app.route('/accept-list', methods=['POST'])
@login_required
def accept_list():
list_id = request.json.get('list_id')
habit_list = HabitList.get(int(list_id))
users = habit_list.get_users()
# Check if user is part of the list
found = False
for user in users:
if user.id == current_user.id:
found = True
break
if not found:
return {}
current_user.accept_List(habit_list.id)
return {}
@app.route('/deny-list', methods=['POST'])
@login_required
def deny_list():
list_id = request.json.get('list_id')
habit_list = HabitList.get(int(list_id))
users = habit_list.get_users()
# Check if user is part of the list
found = False
for user in users:
if user.id == current_user.id:
found = True
break
if not found:
return {}
habit_list.remove_user(current_user.id)
return {}
@app.route('/reorder-list', methods=['POST'])
@login_required
def reorder_habit_list():
new_index = request.get_json()["newIndex"] + 1
habitList = HabitList.get(request.get_json()["listId"])
if habitList is None:
return {"error": "HabitList not found"}
# Check if habit belongs to user
users = habitList.get_users()
if current_user not in users:
return {"error": "HabitList does not belong to user"}
habitList.update_slot(current_user.id, new_index)
return {}
###########################################################
######################### Profile #########################
@app.route('/profile')
@login_required
def profile():
@ -273,9 +734,10 @@ def profile():
name=current_user.name,
email=current_user.email,
profile_image_url=current_user.profile_image,
color = current_user.heatmap_color,
title="Profil",
)
@app.route('/profile', methods=['POST'])
@login_required
def profile_change():
@ -293,6 +755,7 @@ def profile_change():
name=current_user.name,
email=current_user.email,
profile_image_url=current_user.profile_image,
color=current_user.heatmap_color,
)
@app.route('/check_password', methods=['POST'])
@ -317,16 +780,13 @@ def password_change():
current_user.update()
# Back to profile
return render_template(
"profile.html",
return render_template("profile.html",
name=current_user.name,
email=current_user.email,
profile_image_url=current_user.profile_image,
color=current_user.heatmap_color,
)
UPLOAD_FOLDER = 'static/profile_images/' # Folder to store profile images
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def save_profile_image(image_file):
filename = image_file.filename
if '.' not in filename:
@ -345,35 +805,65 @@ def save_profile_image(image_file):
# Open the uploaded image
image = Image.open(image_file)
# Convert the image to RGB mode (required for JPEG)
image = image.convert('RGB')
# Function to crop and resize frames
def process_frame(frame, size):
new_size = min(frame.size)
left = (frame.width - new_size) // 2
top = (frame.height - new_size) // 2
right = left + new_size
bottom = top + new_size
cropped_frame = frame.crop((left, top, right, bottom))
return cropped_frame.resize(size)
# Determine the size of the square image
min_dimension = min(image.size)
square_size = (min_dimension, min_dimension)
# Function to process frames in parallel
def process_frames_parallel(frames, size):
with concurrent.futures.ThreadPoolExecutor() as executor:
resized_frames = list(executor.map(lambda f: process_frame(f, size), frames))
return resized_frames
# Calculate the coordinates for cropping
left = (image.width - min_dimension) / 2
top = (image.height - min_dimension) / 2
right = (image.width + min_dimension) / 2
bottom = (image.height + min_dimension) / 2
# Check if the image is an animated gif
if file_extension == 'gif':
# Process frames
gif_frames = [frame.copy() for frame in ImageSequence.Iterator(image)]
processed_frames = process_frames_parallel(gif_frames, size=(128, 128))
# Crop the image to a square and resize it
image = image.crop((left, top, right, bottom))
image.thumbnail(square_size)
image = image.resize((256, 256))
# Save the modified frames as a new GIF
output_gif_path = os.path.join(app.config['UPLOAD_FOLDER'], filename.replace(".jpg", ".gif"))
processed_frames[0].save(output_gif_path, save_all=True, append_images=processed_frames[1:], loop=0)
return output_gif_path
else:
# Process single image
processed_image = process_frame(image, size=(256, 256))
processed_image = processed_image.convert('RGB')
# Save the processed image
image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
image.save(image_path, 'JPEG', quality=100)
image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename.replace(".gif", ".jpg"))
processed_image.save(image_path, 'JPEG', quality=100)
return image_path
@app.route('/upload', methods=['POST'])
@login_required
def upload_profile_image():
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
save_profile_image(file)
image_path = save_profile_image(file)
# Update the User
current_user.profile_image = image_path
current_user.update()
# Back to profile
return redirect(url_for('profile'))
@app.route('/save_color', methods=['POST'])
@login_required
def save_heatmap_color():
# Get the color value from the form
new_color = request.form['color']
current_user.heatmap_color = new_color
current_user.update()
# Back to profile
return render_template(
@ -381,83 +871,19 @@ def upload_profile_image():
name=current_user.name,
email=current_user.email,
profile_image_url=current_user.profile_image,
errors={}
color=current_user.heatmap_color,
)
@app.route('/check', methods=['POST'])
@app.route('/delete_account', methods=['POST'])
@login_required
def check_habit():
habit_id = request.get_json()["habitId"]
def delete_account():
os.remove(current_user.profile_image)
current_user.delete()
habit = Habit.get(habit_id)
if habit is None:
return {"error": "Habit not found"}
# Check if habit belongs to user
users = habit.habit_list().get_users()
if current_user not in users:
return {"error": "Habit does not belong to user"}
trackings = habit.get_habitTracking()
# Check if habit has been tracked today
delete_tracking = None
for tracking in trackings:
if tracking.created_at.date() == datetime.date.today():
delete_tracking = tracking
if not delete_tracking:
HabitTracking.create(habit_id)
else:
delete_tracking.delete()
# Update habit
habit.fill_statistics()
return {
"habitId": habit_id,
"unchecked": not delete_tracking,
"percentage": habit.percentage,
}
@app.route('/delete', methods=['POST'])
@login_required
def delete_habit():
habit_id = request.get_json()["habitId"]
habit = Habit.get(habit_id)
if habit is None:
return {"error": "Habit not found"}
# Check if habit belongs to user
if current_user not in habit.habit_list().get_users():
return {"error": "Habit does not belong to user"}
habit.delete()
return {}
@app.route('/reorder', methods=['POST'])
@login_required
def reorder_habits():
new_index = request.get_json()["newIndex"]+1
habit = Habit.get(request.get_json()["habitId"])
if habit is None:
return {"error": "Habit not found"}
# Check if habit belongs to user
users = habit.habit_list().get_users()
if current_user not in users:
return {"error": "Habit does not belong to user"}
habit.update_slot(new_index)
return {}
return redirect(url_for('index'))
###########################################################
# Run the application
if __name__ == '__main__':
app.run(port=5000, debug=True)
app.run(host="0.0.0.0", port=5000, debug=True)

View File

@ -27,11 +27,11 @@ def create_user_profile_image(user_id):
return relative_destination_path
def create_user(name: str, email: str, password: str, profile_image:str = None):
def create_user(name: str, email: str, password: str, heatmap_color: str, profile_image: str = None):
password = hashlib.sha256(password.encode()).hexdigest()
now = datetime.now().isoformat()
query = (f"INSERT INTO users (name, email, password, profile_image, created_at, updated_at) VALUES "
f"('{name}', '{email}', '{password}', '{profile_image}', '{now}', '{now}');")
query = (f"INSERT INTO users (name, email, password, profile_image, heatmap_color, created_at, updated_at) VALUES "
f"('{name}', '{email}', '{password}', '{profile_image}', '{heatmap_color}', '{now}', '{now}');")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
@ -66,13 +66,15 @@ def get_user_by_email(email: str):
return user
def update_user(id: int, name: str, email: str, password: str):
def update_user(id: int, name: str, email: str, password: str, profile_image: str, heatmap_color: str):
now = datetime.now().isoformat()
if password:
query = (f"UPDATE users SET name = '{name}', email = '{email}', password = '{password}', updated_at = '{now}' "
query = (f"UPDATE users SET name = '{name}', email = '{email}', password = '{password}', "
f"profile_image ='{profile_image}', heatmap_color = '{heatmap_color}', updated_at = '{now}' "
f"WHERE id = {id};")
else:
query = f"UPDATE users SET name = '{name}', email = '{email}', updated_at = '{now}' WHERE id = {id};"
query = (f"UPDATE users SET name = '{name}', email = '{email}', profile_image ='{profile_image}', "
f"heatmap_color = '{heatmap_color}', updated_at = '{now}' WHERE id = {id};")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
@ -92,10 +94,10 @@ def delete_user(id: int):
### Habit ###
def create_habit(list_id: int, name: str, note: str, times: int, unit: int, slot: int):
def create_habit(list_id: int, name: str, note: str, times: int, unit: int, slot: int, checked: bool, count:int, streak: int):
now = datetime.now().isoformat()
query = (f"INSERT INTO habits (list_id, name, note, times, unit, slot, created_at, updated_at) "
f"VALUES ('{list_id}', '{name}', '{note}', '{times}', '{unit}', '{slot}', '{now}', '{now}');")
query = (f"INSERT INTO habits (list_id, name, note, times, unit, slot, checked, count, streak, created_at, updated_at) "
f"VALUES ('{list_id}', '{name}', '{note}', '{times}', '{unit}', '{slot}', '{checked}', '{count}', '{streak}', '{now}', '{now}');")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
@ -126,7 +128,6 @@ def get_habits(list_id: int):
def get_heatmap_value(user_id: int, days: int):
date = (datetime.now() - timedelta(days=days)).date()
print(date)
# Uses JOINs to get all Habits
query = (f"SELECT habits.id FROM habits "
@ -158,7 +159,7 @@ def get_heatmap_value(user_id: int, days: int):
return 0
def get_next_slot(list_id: int):
def habit_get_next_slot(list_id: int):
query = f"SELECT slot FROM habits WHERE list_id = {list_id} ORDER BY slot DESC LIMIT 1;"
conn = con3()
cursor = conn.cursor()
@ -168,7 +169,7 @@ def get_next_slot(list_id: int):
return slot[0] + 1 if slot else 1
def get_slots(list_id: int):
def habit_get_slots(list_id: int):
query = f"SELECT id, slot FROM habits WHERE list_id = {list_id} ORDER BY slot;"
conn = con3()
cursor = conn.cursor()
@ -178,7 +179,7 @@ def get_slots(list_id: int):
return slots
def update_slot(id: int, slot: int):
def habit_update_slot(id: int, slot: int):
now = datetime.now().isoformat()
query = f"UPDATE habits SET slot = {slot}, updated_at = '{now}' WHERE id = {id};"
conn = con3()
@ -191,7 +192,19 @@ def update_slot(id: int, slot: int):
def update_habit(id: int, name: str, note: str, times: int, unit: int):
now = datetime.now().isoformat()
query = (f"UPDATE habits SET name = {name}, note = {note}, times = {times}, unit = {unit}, updated_at = '{now}' "
query = (f"UPDATE habits SET name = '{name}', note = '{note}', times = {times}, unit = {unit}, updated_at = '{now}' "
f"WHERE id = {id};")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
conn.commit()
conn.close()
return cursor.lastrowid
def update_habit_statistics(id: int, checked: bool, count: int, streak: int):
now = datetime.now().isoformat()
query = (f"UPDATE habits SET checked = {checked}, count = {count}, streak = {streak}, updated_at = '{now}' "
f"WHERE id = {id};")
conn = con3()
cursor = conn.cursor()
@ -252,15 +265,15 @@ def delete_habitTracking(id: int):
### HabitList ###
def create_habitList(user_id: int, name: str, description: str):
def create_habitList(user_id: int, name: str, description: str, slot: int):
now = datetime.now().isoformat()
query = (f"INSERT INTO habit_lists (name, description, created_at, updated_at) "
f"VALUES ('{name}', '{description}', '{now}', '{now}');")
query = (f"INSERT INTO habit_lists (name, description, slot, created_at, updated_at) "
f"VALUES ('{name}', '{description}', '{slot}', '{now}', '{now}');")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
query2 = (f"INSERT INTO habit_users (user_id, list_id, created_at, updated_at)"
f" VALUES ('{user_id}', '{cursor.lastrowid}', '{now}', '{now}');")
query2 = (f"INSERT INTO habit_users (user_id, list_id, created_at, updated_at, accepted)"
f" VALUES ('{user_id}', '{cursor.lastrowid}', '{now}', '{now}', 1);")
cursor.execute(query2)
conn.commit()
conn.close()
@ -277,8 +290,41 @@ def get_habitList(id: int):
return habit_list
def habitList_get_next_slot(user_id: int):
query = (f"SELECT slot FROM habit_lists JOIN habit_users ON habit_lists.id = habit_users.list_id "
f"WHERE habit_users.user_id = {user_id} ORDER BY slot DESC LIMIT 1;")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
slot = cursor.fetchone()
conn.close()
return slot[0] + 1 if slot else 1
def habitList_get_slots(user_id: int):
query = (f"SELECT habit_lists.id, slot FROM habit_lists JOIN habit_users ON habit_lists.id = habit_users.list_id "
f"WHERE habit_users.user_id = {user_id} ORDER BY slot;")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
slots = cursor.fetchall()
conn.close()
return slots
def habitList_update_slot(id: int, slot: int):
now = datetime.now().isoformat()
query = f"UPDATE habit_lists SET slot = {slot}, updated_at = '{now}' WHERE id = {id};"
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
conn.commit()
conn.close()
return cursor.lastrowid
def get_habitLists(user_id: int):
query = (f"SELECT habit_lists.* FROM habit_lists JOIN habit_users ON habit_lists.id = habit_users.list_id "
query = (f"SELECT habit_lists.*, habit_users.accepted FROM habit_lists JOIN habit_users ON habit_lists.id = habit_users.list_id "
f"WHERE habit_users.user_id = {user_id};")
conn = con3()
cursor = conn.cursor()
@ -288,6 +334,17 @@ def get_habitLists(user_id: int):
return habit_lists
def get_unaccepted_habitLists(user_id: int):
query = (f"SELECT habit_lists.* FROM habit_lists JOIN habit_users ON habit_lists.id = habit_users.list_id "
f"WHERE habit_users.user_id = {user_id} AND habit_users.accepted = false;")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
habit_lists = cursor.fetchall()
conn.close()
return habit_lists
def get_users(list_id: int):
query = (f"SELECT users.* FROM users JOIN habit_users ON users.id = habit_users.user_id WHERE "
f"habit_users.list_id = {list_id};")
@ -310,8 +367,17 @@ def add_user(list_id: int, user_id: int):
conn.close()
def accept_List(list_id: int, user_id: int):
query = f"UPDATE habit_users SET accepted = 1 WHERE habit_users.user_id = {user_id} AND habit_users.list_id = {list_id};"
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
conn.commit()
conn.close()
def remove_user(list_id: int, user_id: int):
query = f"DELETE FROM habit_lists WHERE user_id = {user_id} AND list_id = {list_id};"
query = f"DELETE FROM habit_users WHERE user_id = {user_id} AND list_id = {list_id};"
conn = con3()
cursor = conn.cursor()
cursor.execute(query)

View File

@ -0,0 +1 @@
DROP TABLE habits;

View File

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS habits
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
list_id INTEGER NOT NULL,
name TEXT NOT NULL,
note TEXT,
times INTEGER NOT NULL,
unit INTEGER,
slot INTEGER NOT NULL,
checked BOOLEAN NOT NULL,
count INTEGER NOT NULL,
streak INTEGER NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (list_id) REFERENCES habit_lists(id)
);

View File

@ -0,0 +1 @@
DROP TABLE users;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS users
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
password TEXT NOT NULL,
profile_image TEXT NOT NULL,
heatmap_color TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);

View File

@ -0,0 +1,2 @@
ALTER TABLE habit_users
ADD COLUMN accepted BOOLEAN DEFAULT false;

View File

@ -0,0 +1 @@
DROP TABLE habit_lists;

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS habit_lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
slot INTEGER NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);

View File

@ -1,10 +1,11 @@
import json
from dataclasses import dataclass
from datetime import datetime
from datetime import timedelta
from models.HabitTracking import HabitTracking
from db.SQLiteClient import (create_habit, get_habit, update_habit, delete_habit, get_next_slot, get_slots, update_slot,
get_habitTrackings, get_habitList)
from db.SQLiteClient import (create_habit, get_habit, update_habit, delete_habit, habit_get_next_slot, habit_get_slots,
habit_update_slot, get_habitTrackings, get_habitList, update_habit_statistics)
# unit will be represented by integers like this:
@ -22,21 +23,24 @@ class Habit:
times: int
unit: int
slot: int
checked: bool
count: int
streak: int
percentage: int = 0
def __post_init__(self):
self.fill_statistics()
self.load_statistics()
@staticmethod
def create(list_id: int, name: str, times: int, note: str = None, unit: int = 1):
slot = get_next_slot(list_id)
id = create_habit(list_id, name, note, times, unit, slot)
return Habit(id, list_id, name, note, times, unit, slot)
def create(list_id: int, name: str, times: int, note: str = None, unit: int = 1, checked: bool = False, count: int = 0, streak: int = 0):
slot = habit_get_next_slot(list_id)
id = create_habit(list_id, name, note, times, unit, slot, checked, count, streak)
return Habit(id, list_id, name, note, times, unit, slot, checked, count, streak)
@staticmethod
def get(id: int):
habit = get_habit(id)
return Habit(habit[0], habit[1], habit[2], habit[3], habit[4], habit[5], habit[6]) if habit else None
return Habit(habit[0], habit[1], habit[2], habit[3], habit[4], habit[5], habit[6], habit[7], habit[8], habit[9]) if habit else None
# Updates: name, note, times, unit
@ -47,28 +51,28 @@ class Habit:
# Updates the slot and reorders the HabitList accordingly
def update_slot(self, new_slot: int):
# Fetches a list with the following structure [(id, slot), (id, slot), ...]
slots = get_slots(self.list_id)
slots = habit_get_slots(self.list_id)
# Splits the list depending on whether the new slot is higher or lower than the current one
if new_slot > self.slot: # Example self.slot=1 new_slot=4
slots = slots[self.slot:new_slot] # Expected list: [(id, 2), (id, 3), (id, 4)]
for slot in slots:
update_slot(slot[0], slot[1]-1)
habit_update_slot(slot[0], slot[1]-1)
if new_slot < self.slot: # Example self.slot=4 new_slot=1
slots = slots[new_slot-1:self.slot-1] # Expected list: [(id, 1), (id, 2), (id, 3)]
for slot in slots:
update_slot(slot[0], slot[1]+1)
habit_update_slot(slot[0], slot[1]+1)
# Update the slot of the current habit
update_slot(self.id, new_slot)
habit_update_slot(self.id, new_slot)
# Deletes the Habit
def delete(self):
# Reorders the slots
slots = get_slots(self.list_id)[self.slot+1:]
slots = habit_get_slots(self.list_id)[self.slot+1:]
for slot in slots:
update_slot(slot[0], slot[1] - 1)
habit_update_slot(slot[0], slot[1] - 1)
# Deletes all track-records associated with the Habit
trackings = self.get_habitTrackings()
@ -92,37 +96,47 @@ class Habit:
def habit_list(self) -> list:
from models.HabitList import HabitList
raw_habitLists = get_habitList(self.list_id)
return HabitList(raw_habitLists[0], raw_habitLists[1], raw_habitLists[2]) if raw_habitLists else None
return HabitList(raw_habitLists[0], raw_habitLists[1], raw_habitLists[2], raw_habitLists[3]) if raw_habitLists else None
# Saves the progress of the Habit in the attribute percentage
def fill_statistics(self):
count = 0
# Loads the progress and checks if the streak is not broken
def load_statistics(self):
today = datetime.today().date()
yesterday = today - timedelta(days=1)
tracking_dates = [tracking.created_at.date() for tracking in self.get_habitTrackings()]
if not today in tracking_dates:
self.checked = False
for tracking in self.get_habitTrackings():
if tracking.created_at.date() == datetime.today().date():
self.checked = True
if not yesterday in tracking_dates:
self.streak = 0
update_habit_statistics(self.id, self.count, self.count, self.streak)
# day
# Reset count based on time unit
if self.unit == 0:
if tracking.created_at.date() == datetime.today().date():
count += 1
# week
elif self.unit == 1:
if tracking.created_at.isocalendar()[1] == datetime.today().isocalendar()[1]:
count += 1
# month
elif self.unit == 2:
if tracking.created_at.month == datetime.today().month:
count += 1
# year
elif self.unit == 3:
if tracking.created_at.year == datetime.today().year:
count += 1
self.count = 0
elif self.unit == 1 and today.weekday() == 0:
self.count = 0
elif self.unit == 2 and today.day == 1:
self.count = 0
elif self.unit == 3 and today.month == 1 and today.day == 1:
self.count = 0
self.percentage = int(count / self.times * 100)
self.percentage = int(self.count / self.times * 100)
# Saves the progress count and streak
def fill_statistics(self):
self.checked = True
self.streak += 1
self.count += 1
update_habit_statistics(self.id, self.checked, self.count, self.streak)
# Turns the statistics back to the unchecked state
def reset_statistics(self):
self.checked = False
self.streak -= 1
self.count -= 1
update_habit_statistics(self.id, self.checked, self.count, self.streak)
# Converts the Habit data to a json format
def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)

View File

@ -3,7 +3,8 @@ from dataclasses import dataclass
from models.Habit import Habit
from models.User import User
from db.SQLiteClient import (create_habitList, get_habitList, get_habits, get_users, add_user, remove_user,
update_habitList, delete_habitList)
update_habitList, delete_habitList, habitList_get_next_slot, habitList_get_slots,
habitList_update_slot)
@dataclass
@ -11,17 +12,19 @@ class HabitList:
id: int
name: str
description: str
habits: list = None #? unclear usage
slot: int
habits: list = None
@staticmethod
def create(user_id: int, name: str, description: str):
id = create_habitList(user_id, name, description)
return HabitList(id, name, description)
slot = habitList_get_next_slot(user_id)
id = create_habitList(user_id, name, description, slot)
return HabitList(id, name, description, slot)
@staticmethod
def get(id: int):
habitList = get_habitList(id)
return HabitList(habitList[0], habitList[1], habitList[2]) if habitList else None
return HabitList(habitList[0], habitList[1], habitList[2], habitList[3]) if habitList else None
# Updates: name, description
@ -29,11 +32,37 @@ class HabitList:
update_habitList(self.id, self.name, self.description)
# Updates the slot and reorders the HabitLists accordingly
def update_slot(self,user_id: int, new_slot: int):
# Fetches a list with the following structure [(id, slot), (id, slot), ...]
slots = habitList_get_slots(user_id)
# Splits the list depending on whether the new slot is higher or lower than the current one
if new_slot > self.slot: # Example self.slot=1 new_slot=4
slots = slots[self.slot:new_slot] # Expected list: [(id, 2), (id, 3), (id, 4)]
for slot in slots:
habitList_update_slot(slot[0], slot[1]-1)
if new_slot < self.slot: # Example self.slot=4 new_slot=1
slots = slots[new_slot-1:self.slot-1] # Expected list: [(id, 1), (id, 2), (id, 3)]
for slot in slots:
habitList_update_slot(slot[0], slot[1]+1)
# Update the slot of the current habitList
habitList_update_slot(self.id, new_slot)
# Deletes the HabitList | The id of the current user is necessary
def delete(self, user_id):
if len(get_users) > 1:
# Reorders the slots
slots = habitList_get_slots(user_id)[self.slot+1:]
for slot in slots:
habitList_update_slot(slot[0], slot[1] - 1)
if len(get_users(self.id)) > 1:
self.remove_user(user_id)
else:
for habit in self.get_habits():
habit.delete()
delete_habitList(self.id)
@ -42,7 +71,7 @@ class HabitList:
raw_habits = get_habits(self.id)
habits = []
for habit in raw_habits:
habit = Habit(habit[0], habit[1], habit[2], habit[3], habit[4], habit[5], habit[6])
habit = Habit(habit[0], habit[1], habit[2], habit[3], habit[4], habit[5], habit[6], habit[7], habit[8], habit[9])
habits.append(habit)
return habits
@ -53,17 +82,18 @@ class HabitList:
raw_users = get_users(self.id)
users = []
for user in raw_users:
user = User(user[0], user[1], user[2], user[3])
user = User(user[0], user[1], user[2], user[3], user[4], user[5])
users.append(user)
return users
# Adds a User by email to the HabitList
def add_user(self, email: str):
user = User.get_by_email(email)
def add_user(self, user: User):
if user:
add_user(self.id, user.id)
else:
return None
# Removes a User from the HabitList
def remove_user(self, user_id):

View File

@ -1,37 +1,40 @@
from datetime import datetime
from flask_login import UserMixin
from db.SQLiteClient import (create_user, get_user, get_user_by_email, update_user, delete_user,
get_habitLists, get_heatmap_value)
get_habitLists, get_heatmap_value, accept_List, get_unaccepted_habitLists)
class User(UserMixin):
def __init__(self, id: int, name: str, email: str, password: str = None, profile_image:str = None):
def __init__(self, id: int, name: str, email: str, password: str=None, profile_image:str=None, heatmap_color: str=None):
self.id = id
self.name = name
self.email = email
self.password = password
self.profile_image = profile_image
self.heatmap_color = heatmap_color
@staticmethod
def create(name: str, email: str, password: str):
id, profile_image = create_user(name, email, password)
return User(id=id, name=name, email=email, profile_image=profile_image)
heatmap_color = "#00FF00"
id, profile_image = create_user(name, email, password, heatmap_color)
return User(id=id, name=name, email=email, profile_image=profile_image, heatmap_color=heatmap_color)
@staticmethod
def get(id: int):
user = get_user(id)
return User(user[0], user[1], user[2], user[3], user[4]) if user else None
return User(user[0], user[1], user[2], user[3], user[4], user[5]) if user else None
@staticmethod
def get_by_email(email: str):
user = get_user_by_email(email)
return User(user[0], user[1], user[2], user[3], user[4]) if user else None
return User(user[0], user[1], user[2], user[3], user[4], user[5]) if user else None
# Updates: name, email, password
def update(self):
update_user(self.id, self.name, self.email, self.password if self.password else None)
update_user(self.id, self.name, self.email, self.password if self.password else None, self.profile_image, self.heatmap_color)
# Deletes the User
def delete(self):
@ -51,16 +54,42 @@ class User(UserMixin):
raw_habitLists = get_habitLists(self.id)
habitLists = []
for habitList in raw_habitLists:
habitList = HabitList(habitList[0], habitList[1], habitList[2])
accepted = habitList[6]
habitList = HabitList(habitList[0], habitList[1], habitList[2], habitList[3])
if accepted == 1:
habitLists.append(habitList)
return habitLists
def get_unaccepted_habitLists(self) -> list:
from models.HabitList import HabitList
raw_habitLists = get_unaccepted_habitLists(self.id)
habitLists = []
for habitList in raw_habitLists:
habitList = HabitList(habitList[0], habitList[1], habitList[2], habitList[3])
habitLists.append(habitList)
return habitLists
# Returns all heatmap-values from the last 28 days
def get_heatmap(self) -> list:
heatmap = []
for day in range (0, 28):
def get_heatmap(self) -> tuple:
# get current day of week as integer. monday is 0 and sunday is 6
weekday = 6 - datetime.today().weekday()
heatmap = [100]
# append the heatmap values of the current week
for day in range(0, weekday):
heatmap.append(0)
for day in range (0, 28-weekday):
value = get_heatmap_value(self.id, day)
heatmap.append(value)
return heatmap
heatmap.reverse()
day = 27-weekday
return heatmap, day
def accept_List(self, HabitList_id):
accept_List(HabitList_id, self.id)

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
pillow~=10.2.0
flask~=3.0.0
flask-login~=0.6.3

73
static/css/heatmap.css Normal file
View File

@ -0,0 +1,73 @@
#heatmap {
display: grid;
grid-template-columns: repeat(7, 0fr); /* 7 Tage in einer Woche */
gap: 5px;
width: 100%;
table-layout: fixed;
}
.day {
width: 50px;
height: 50px;
border: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
table-layout: fixed;
}
@media (max-width: 1400px) {
.day {
width: 40px;
height: 40px;
}
}
@media (max-width: 1200px) {
.day {
width: 35px;
height: 35px;
}
}
@media (max-width: 992px) {
.day {
width: 30px;
height: 30px;
}
}
@media (max-width: 767px) {
.day {
width: 50px;
height: 50px;
}
}
@media (max-width: 450px) {
.day {
width: 40px;
height: 40px;
}
}
@media (max-width: 400px) {
.day {
width: 35px;
height: 35px;
}
}
@media (max-width: 350px) {
.day {
width: 30px;
height: 30px;
}
}
@media (max-width: 300px) {
.day {
width: 25px;
height: 25px;
}
}

BIN
static/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,213 @@
// Erstellen der Heatmap
function createHeatmap(data, day) {
const heatmapContainer = document.getElementById('heatmap');
const days = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
for (let i = 0; i < 7; i++) {
const dayElement = document.createElement('div');
dayElement.classList.add('day');
dayElement.textContent = days[i];
heatmapContainer.appendChild(dayElement);
}
// Aktuelles Datum des Montags in der neuen linken Spalte
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 7; j++) {
const opacity = data[i * 7 + j] / (Math.max(...data) <= 0 ? 1 : Math.max(...data)); // Berechne die Opazität basierend auf Aktivitätsanzahl
const dayElement = document.createElement('div');
dayElement.classList.add('day');
dayElement.style.backgroundColor = `rgba(${color}, ${opacity})`;
if (day === i * 7 + j){
dayElement.style.borderColor = `rgba(0, 0, 0)`;
dayElement.style.borderWidth = "2px";
}
heatmapContainer.appendChild(dayElement);
}
}
}
// Löschen der Heatmap
function deleteHeatmap() {
const heatmapContainer = document.getElementById('heatmap');
const dayElements = heatmapContainer.getElementsByClassName('day');
// Convert HTMLCollection to array and iterate to remove each element
Array.from(dayElements).forEach(element => {
element.remove();
});
}
// Animation der Progressbar
function checkCompletionAndAnimate(habitId, percentage) {
const progressBar = document.getElementById("progress-bar-" + habitId);
const habitBlock = document.getElementById("habit-" + habitId);
if (percentage === 100) {
progressBar.style.backgroundColor = "green";
habitBlock.classList.add("animate-bounce");
setTimeout(function () {
habitBlock.classList.remove("animate-bounce");
}, 2000);
} else {
progressBar.style.backgroundColor = "";
habitBlock.classList.remove("animate-bounce");
}
}
// Senden einer Post-Request, sobald ein Habit abgehackt wird
function sendPostRequest(checkboxId) {
// Get the habit id from the checkbox id attribute
const habitId = checkboxId;
// Make a POST request to /check with the habit id
axios.post('/check', {habitId: habitId}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
console.log(response.data);
// Set the percentage of the habit. percentage received as integer
const percentage = response.data.percentage;
const progressBar = document.getElementById("progress-bar-" + habitId);
progressBar.style.width = percentage + "%";
checkCompletionAndAnimate(habitId, percentage);
const streak = response.data.streak;
const streakSymbol = document.getElementById("streak-" + habitId);
streakSymbol.innerText = streak > 0 ? streak.toString() + " 🔥" : "";
const heatmapValues = response.data.heatmap;
deleteHeatmap()
createHeatmap(heatmapValues, day, color)
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
// Senden einer Post-Request, sobald ein Habit gelöscht wird
function deleteHabit(habitId) {
// Make a POST request to /delete with the habit id
axios.post('/delete', {habitId: habitId}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
console.log(response.data);
// Remove the habit from the DOM
const habitElement = document.getElementById("habit-" + habitId);
habitElement.remove();
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
// Senden einer Post-Request, sobald eine HabitList gelöscht wird
function deleteList(listId) {
// Make a POST request to /delete with the habit id
axios.post('/delete-list', {listId: listId}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
console.log(response.data);
// Remove the habit from the DOM
let habitElement = document.getElementById("simple-tabpanel-" + listId);
habitElement.remove();
habitElement = document.getElementById("tab-" + listId);
habitElement.remove();
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
document.addEventListener('DOMContentLoaded', () => {
const elements = document.querySelectorAll('.task-list').values()
// loop through the elements
for (let el of elements) {
Sortable.create(el, {
handle: '.drag-handle',
animation: 150,
onEnd: function (evt) {
const habitId = el.children[evt.newIndex].id.split('-')[1];
const oldIndex = evt.oldIndex;
const newIndex = evt.newIndex;
axios.post('/reorder-habit', {habitId: habitId, oldIndex: oldIndex, newIndex: newIndex}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
});
}
});
document.addEventListener('DOMContentLoaded', () => {
const listElements = document.querySelectorAll('.nav-tabs').values();
// Loop through the list elements
for (let listEl of listElements) {
Sortable.create(listEl, {
handle: '.nav-link', // Use the nav-link as the handle for dragging
animation: 150,
onEnd: function (evt) {
const listId = evt.item.id.split('-')[1];
const oldIndex = evt.oldIndex;
const newIndex = evt.newIndex;
// Send a POST request to reorder the list
axios.post('/reorder-list', { listId: listId, oldIndex: oldIndex, newIndex: newIndex}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
});
}
});
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
// Aktualisieren der Uhrzeit
function updateCurrentTime() {
const currentTimeElement = document.getElementById('current-time');
const currentDate = new Date();
const options = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false };
const currentDateTime = currentDate.toLocaleString('de-DE', options);
currentTimeElement.innerText = currentDateTime.replace(',', ',') + ' ' + currentDate.toLocaleString('de-DE', { weekday: 'long' });
}
// Erstellt die Heatmap mit den simulierten Daten
createHeatmap(activityData, day, color);
updateCurrentTime();
setInterval(updateCurrentTime, 1000);

View File

@ -0,0 +1,195 @@
document.addEventListener("DOMContentLoaded", function() {
// Get elements
const profileImage = document.getElementById("profileImage");
const profileImageOverlay = document.getElementById("profileImageOverlay");
const profileImageInput = document.getElementById("profileImageInput");
const uploadForm = document.getElementById("uploadForm");
const editButton = document.getElementById("editButton");
const saveChangesButton = document.getElementById("saveChangesButton");
const editForm = document.getElementById("editForm");
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
const submitPasswordChangeButton = document.getElementById("submitPasswordChange");
const DeleteAccountButton = document.getElementById('deleteAccountButton')
const confirmDeleteModal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
const ConfirmDeleteButton = document.getElementById('confirmDeleteButton')
const deleteAccountForm = document.getElementById('deleteAccountForm')
// Open file input when profile image is clicked
profileImageOverlay.addEventListener("click", function() {
profileImageInput.click();
});
// Change profile image when a new file is selected
profileImageInput.addEventListener("change", function() {
const file = this.files[0];
const reader = new FileReader();
reader.onload = function(e) {
profileImage.src = e.target.result;
};
reader.readAsDataURL(file);
// Submit the form
uploadForm.submit();
});
// Add event listener to edit button to open modal
editButton.addEventListener("click", function() {
editModal.show();
});
// Add event listener to save changes button to submit form
saveChangesButton.addEventListener("click", function() {
// Perform client-side validation before submitting the form
validateForm()
.then(isValid => {
if (isValid) {
editForm.submit();
}
})
.catch(error => {
// Handle validation error
console.log("Account Form validation failed", error);
});
});
// Function to perform client-side validation
async function validateForm() {
let isValid = true;
isValid = validateInput("newName", "nameFeedback", "Bitte geben Sie einen neuen Namen ein.") && isValid;
isValid = validateEmail("newEmail", "emailFeedback", "Bitte geben Sie eine gültige E-Mail-Adresse ein.") && isValid;
try {
const passwordValid = await validatePassword("password", "passwordFeedback", "Bitte geben Sie Ihr Passwort ein.");
isValid = passwordValid && isValid;
} catch (error) {
isValid = false;
}
return isValid;
}
// Function to validate input fields
function validateInput(inputId, feedbackId, errorMessage) {
const input = document.getElementById(inputId);
const feedback = document.getElementById(feedbackId);
if (!input.value.trim()) {
feedback.textContent = errorMessage;
input.classList.add("is-invalid");
return false;
} else {
feedback.textContent = "";
input.classList.remove("is-invalid");
return true;
}
}
// Function to validate email
function validateEmail(emailId, feedbackId, errorMessage) {
const input = document.getElementById(emailId);
const feedback = document.getElementById(feedbackId);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Regular expression for email validation
if (!input.value.trim()) {
feedback.textContent = "Bitte geben Sie eine neue E-Mail-Adresse ein.";
input.classList.add("is-invalid");
return false;
} else if (!emailRegex.test(input.value.trim())) {
feedback.textContent = errorMessage;
input.classList.add("is-invalid");
return false;
} else {
feedback.textContent = "";
input.classList.remove("is-invalid");
return true;
}
}
// Function to validate password
function validatePassword(passwordId, passwordFeedbackId, errorMessage) {
return new Promise((resolve, reject) => {
const passwordInput = document.getElementById(passwordId);
const passwordFeedback = document.getElementById(passwordFeedbackId);
const password = passwordInput.value; // Get the password entered by the user
if (!passwordInput.value.trim()) {
passwordFeedback.textContent = errorMessage;
passwordInput.classList.add("is-invalid");
reject(false);
} else {
axios.post('/check_password', { password: password })
.then(response => {
if (!response.data.valid) {
passwordFeedback.textContent = "Falsches Passwort.";
passwordInput.classList.add("is-invalid");
passwordInput.value = "";
reject(false);
} else {
passwordInput.classList.remove("is-invalid");
resolve(true);
}
})
.catch(error => {
console.error('Error checking password:', error);
reject(false);
});
}
});
}
submitPasswordChangeButton.addEventListener("click", function() {
// Perform client-side validation before submitting the form
validatePasswordChangeForm()
.then(isValid => {
if (isValid) {
document.getElementById("editPasswordForm").submit();
}
})
.catch(error => {
// Handle validation error
console.log("Password change form validation failed", error);
});
});
async function validatePasswordChangeForm() {
let isValid = true;
try {
const passwordValid = await validatePassword("oldPassword", "oldPasswordFeedback", "Bitte geben Sie Ihr altes Passwort ein.");
isValid = passwordValid && isValid;
} catch (error) {
isValid = false;
}
isValid = validateInput("newPassword", "newPasswordFeedback", "Bitte geben Sie Ihr neues Passwort ein.") && isValid;
isValid = validateInput("confirmPassword", "confirmPasswordFeedback", "Bitte bestätigen Sie Ihr neues Passwort.") && isValid;
// Check if new password matches confirm password
const newPassword = document.getElementById("newPassword").value.trim();
const confirmPassword = document.getElementById("confirmPassword").value.trim();
if (newPassword !== confirmPassword) {
document.getElementById("confirmPasswordFeedback").textContent = "Die Passwörter stimmen nicht überein.";
document.getElementById("confirmPassword").classList.add("is-invalid");
isValid = false;
} else {
document.getElementById("confirmPasswordFeedback").textContent = "";
document.getElementById("confirmPassword").classList.remove("is-invalid");
}
return isValid;
}
// Add event listener to edit button to open modal
DeleteAccountButton.addEventListener("click", function() {
confirmDeleteModal.show();
});
//Submit delete account form
ConfirmDeleteButton.addEventListener('click', function () {
deleteAccountForm.submit();
});
});

View File

@ -1,8 +1,8 @@
{% extends 'layouts/main.html' %}
{% block content %}
<div class="card">
<h5 class="card-header">Login</h5>
<div class="card bg-light mt-4">
<h1 class="card-header">Login</h1>
<div class="card-body column">
<form method="POST" action="/login">
<div class="mb-3 row">

View File

@ -1,26 +1,30 @@
{% extends 'layouts/main.html' %}
{% block content %}
<div class="column">
<h3>Registrieren</h3>
<div class="card bg-light mt-4 p-5">
<div class="column">
<h1>Registrieren</h1>
<form method="POST" action="/signup">
<div class="mb-3">
<label for="email" class="form-label">Email-Adresse</label>
<input type="email" class="form-control {% if errors.get('email') %} is-invalid {% endif %}" id="email" name="email" placeholder="name@example.com" value="{{ email }}">
<input type="email" class="form-control {% if errors.get('email') %} is-invalid {% endif %}" id="email"
name="email" placeholder="name@example.com" value="{{ email }}">
<div class="invalid-feedback">
{{ errors.get('email', '') }}
</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control {% if errors.get('name') %} is-invalid {% endif %}" id="name" name="name" placeholder="Max" value="{{ name }}">
<input type="text" class="form-control {% if errors.get('name') %} is-invalid {% endif %}" id="name"
name="name" placeholder="Max" value="{{ name }}">
<div class="invalid-feedback">
{{ errors.get('name', '') }}
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control {% if errors.get('password') %} is-invalid {% endif %}" id="password" name="password" value="{{ password }}">
<input type="password" class="form-control {% if errors.get('password') %} is-invalid {% endif %}"
id="password" name="password" value="{{ password }}">
<div class="invalid-feedback">
{{ errors.get('password', '') }}
</div>
@ -29,5 +33,6 @@
<button type="submit" class="btn btn-primary mb-3">Registrieren</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Bestätige</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Möchtest du dieses Habit wirklich löschen?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">abbrechen</button>
<button type="button" class="btn btn-primary btn-danger" data-bs-dismiss="modal"
onclick="deleteHabit(selectedHabitId)">Löschen
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,20 @@
<div class="modal fade" id="listenModal" tabindex="-1" aria-labelledby="listenModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="listenModalLabel">Bestätige</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Möchtest du diese Liste wirklich löschen?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">abbrechen</button>
<button type="button" class="btn btn-primary btn-danger" data-bs-dismiss="modal"
onclick="deleteList(localStorage.getItem('selectedListId'));">Löschen
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,221 @@
<!--suppress HtmlUnknownTarget -->
<div class="flex-fill col-md-7 col-lg-8 col-12 card bg-light p-6 mb-6">
<!-- Listen erstellen -->
<div class="row mb-3">
<h5 class="col-9">📋 Gewohnheiten</h5>
<a class="col-3 btn btn-primary p" role="button" href="/habit-list">Neue Liste erstellen</a>
</div>
<!-- Tabs zur Auswahl -->
<ul class="nav nav-tabs card-header-tabs" role="tablist">
{% for habit_list in habit_lists %}
<li class="nav-item" role="presentation" id="tab-{{ habit_list.id }}">
<a class="nav-link {% if (active_list is not none and active_list == habit_list.id) or (active_list is none and habit_list == habit_lists[0]) %} active {% endif %}"
id="simple-tab-{{habit_list.id}}"
data-bs-toggle="tab" href="#simple-tabpanel-{{habit_list.id}}" role="tab"
aria-controls="simple-tabpanel-{{habit_list.id}}" aria-selected="true">
{{habit_list.name}}
</a>
</li>
{% endfor %}
</ul>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Select all the tab links
const tabLinks = document.querySelectorAll('.nav-link');
tabLinks.forEach(function(link) {
// Add a click event listener to each tab link
link.addEventListener('click', function(e) {
// Prevent the default action
e.preventDefault();
// Get the tab ID
const tabId = this.getAttribute('href').match(/simple-tabpanel-(\d+)/)[1];
// Update the URL with the new query parameter
const newUrl = updateQueryStringParameter(window.location.href, 'list', tabId);
window.history.pushState({path:newUrl}, '', newUrl);
});
});
// Update the URL with the new query parameter
function updateQueryStringParameter(uri, key, value) {
const re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
const separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
}
else {
return uri + separator + key + "=" + value;
}
}
});
</script>
<div class="tab-content pt-5" id="tab-content">
{% for habit_list in habit_lists %}
<div class="tab-pane {% if (active_list is not none and active_list == habit_list.id) or (active_list is none and habit_list == habit_lists[0]) %} active {% endif %}"
id="simple-tabpanel-{{habit_list.id}}" role="tabpanel" aria-labelledby="simple-tab-{{habit_list.id}}">
<!-- Beschreibung und Löschen von der Liste -->
<div class="row">
<div class="col">
{{ habit_list.description }}
</div>
<div class="col-1">
<button type="button" class="btn btn-xs me-3" data-bs-toggle="modal"
data-bs-target="#listenModal"
onclick="{
localStorage.setItem('selectedListId', {{ habit_list.id }});
}">
<!---onclick="setSelectedListId({{ habit_list.id }})"-->
<i class="bi bi-trash3"></i>
</button>
</div>
</div>
<div class="row mb-3 align-items-center">
<!-- Personen die zur Liste gehören -->
{% if habit_list.get_users()[0].id == current_user.id %}
{% if habit_list.get_users()|length > 1 %}
<div class="col">
<div class="avatar-stack">
{% for user in habit_list.get_users() %}
{% if current_user.id != user.id %}
<img class="avatar" src="/{{user.profile_image}}" data-toggle="tooltip" data-placement="top"
title="{{user.name}}" alt=""/>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
<div class="col">
<!-- Knopf für das Hinzufügen einer Person zur gemeinsamen Liste -->
<a class="me-5" href="/users?habit_list={{habit_list.id}}" style="width: 40px; height: 40px; min-height: 3em;"
data-toggle="tooltip" data-placement="top" title="Benutzer einladen">
<i class="bi bi-person-fill-add" style="font-size: 24px;"></i>
</a>
<!-- Knopf für das Bearbeiten von Personen zur gemeinsamen Liste -->
{% if habit_list.get_users()|length > 1 %}
<a href="/users-edit?habit_list={{habit_list.id}}" style="width: 40px; height: 40px; min-height: 3em;"
data-toggle="tooltip" data-placement="top" title="Benutzer bearbeiten">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
</div>
{% else %}
<div class="row">
<a class="me-5" href="/users-leave?habit_list={{habit_list.id}}" style="width: 40px; height: 40px; min-height: 3em;"
data-toggle="tooltip" data-placement="top" title="Liste verlassen">
<i class="bi bi-box-arrow-left" style="font-size: 24px;"></i>
</a>
<div class="col">
<div class="avatar-stack">
{% for user in habit_list.get_users() %}
{% if current_user.id != user.id %}
<img class="avatar" src="/{{user.profile_image}}" data-toggle="tooltip" data-placement="top"
title="{{user.name}}" alt=""/>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
<div class="col-4"></div>
<!-- neue Gewohnheiten erstellen -->
<a class="col-3 btn btn-primary" role="button" href="/habit?list={{ habit_list.id }}">
Gewohnheit erstellen
</a>
</div>
<ul class="task-list row">
{% for habit in habit_list.habits %}
<li class="row d-flex align-items-center mb-2" id="habit-{{ habit.id }}">
<!-- Handle zum Verschieben -->
<div class="col-auto drag-handle" style="cursor: grab;">
<i class="bi bi-grip-vertical"></i>
</div>
<!-- Checkbox -->
<div class="col-auto">
<input {% if habit.checked %} checked {% endif %} type="checkbox" class="task-checkbox"
id="{{ habit.id }}" onclick="sendPostRequest('{{ habit.id }}')">
</div>
<!-- Name -->
<div class="col" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
{{ habit.name }}
</div>
<!-- Beschreibung -->
<div class="col-md-4 d-none d-md-block text-black text-opacity-50"
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
{{ habit.note }}
</div>
<!-- Streak -->
<div class="col-2" id="streak-{{ habit.id }}"
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
{% if not habit.streak == 0 %}
{{ habit.streak }} 🔥
{% endif %}
</div>
<!-- Knopf für das Löschen einer Gewohnheit -->
<button type="button" class="btn btn-xs me-3" data-bs-toggle="modal"
data-bs-target="#exampleModal" style="width: 40px; height: 40px"
onclick="setSelectedHabitId({{ habit.id }})">
<i class="bi bi-trash3"></i>
</button>
<!-- Knopf für das Bearbeiten der Gewohnheit -->
<a type="button" class="btn" href="{{ url_for('edit_habit') }}?habit={{ habit.id }}"
aria-current="page"
style="width: 40px; height: 40px; min-height: 3em;">
<i class="bi bi-pencil"></i>
</a>
<!-- Progressbar -->
<div class="col-12">
<div class="progress" style="height: 2px; width: 90%">
<div class="progress-bar" id="progress-bar-{{ habit.id }}" role="progressbar"
style="width: {{ habit.percentage }}%; background-color: {% if habit.percentage >= 100 %} green {% else %} primary {% endif %}"
aria-valuenow="{{ habit.percentage }}" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
</div>
<script>
var selectedHabitId = null;
function setSelectedHabitId(habitId) {
selectedHabitId = habitId;
}
var selectedListId = null;
function setSelectedListId(listId) {
selectedlistId = listId;
}
</script>

View File

@ -0,0 +1,20 @@
<div class="flex-fill col-md-5 col-lg-4 col-12 card bg-light mb-6">
<div class="card-body">
<h5 class="card-title">📅 Heatmap</h5>
<div id="heatmap"></div>
</div>
</div>
<script>
function hexToRgb(hex) {
hex = hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (m, r, g, b) => r + r + g + g + b + b);
const [, r, g, b] = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)];
}
// Generates activity based on the Values given by the Backend
const activityData = {{ heatmap_values }};
const day = {{ day }};
const color = hexToRgb("{{ color }}");
</script>

86
templates/edit-habit.html Normal file
View File

@ -0,0 +1,86 @@
{% extends 'index.html' %}
{% block content %}
<div class="card bg-light p-5 mt-5">
<h1>Habit Bearbeiten📋</h1>
<form action="/edit-habit" method="POST">
<div class="mb-3">
<label for="name" class="form-label">Name der Gewohnheit</label>
<input type="text" class="form-control {% if errors.get('name') %} is-invalid {% endif %}" id="name"
name="name" value="{{name}}">
<div class="invalid-feedback">
{{ errors.get('name', '') }}
</div>
</div>
<div class="mb-3">
<label for="note" class="form-label">Beschreibung</label>
<input type="text" class="form-control {% if errors.get('note') %} is-invalid {% endif %}" id="note"
name="note" value="{{note}}">
<div class="invalid-feedback">
{{ errors.get('note', '') }}
</div>
</div>
<div class="row">
<div class="mb-3 col-2">
<label for="times" class="form-label">Häufigkeit</label>
<input type="number" min="1" class="form-control {% if errors.get('times') %} is-invalid {% endif %}"
id="times" name="times" value="{{times}}">
<div class="invalid-feedback">
{{ errors.get('times', '') }}
</div>
</div>
<div class="mb-3 col-10">
<label for="unit" class="form-label">Im Zeitraum</label>
<select class="form-select {% if errors.get('unit') %} is-invalid {% endif %}" id="unit" name="unit">
<option value="Tag">Tag</option>
<option value="Woche">Woche</option>
<option value="Monat">Monat</option>
<option value="Jahr">Jahr</option>
</select>
<script>
document.addEventListener('DOMContentLoaded', () => {
let selectedElement = document.getElementById('unit');
for (let option of selectedElement.options) {
if (option.value === '{{ unit }}') {
option.selected = true;
break;
}
}
});
</script>
<div class="invalid-feedback">
{{ errors.get('unit', '') }}
</div>
</div>
</div>
<input type="hidden" name="habit" id="habit" class="{% if errors.get('habit') %} is-invalid {% endif %}">
<div class="invalid-feedback">
{{ errors.get('list_query', '') }}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Extracting the list-query from the URL
let listQuery = new URLSearchParams(window.location.search).get('habit');
if ("{{ habit }}" !== "") {
listQuery = "{{ habit }}";
// Add the list_id to the URL
const url = new URL(window.location.href);
url.searchParams.set('habit', listQuery);
// window.history.pushState({}, '', url);
}
// Setting the list-query as the value of the hidden input field
document.getElementById('habit').value = listQuery;
});
</script>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% endblock %}

View File

@ -1,8 +1,8 @@
{% extends 'layouts/main.html' %}
{% block content %}
<h1 class="mt-5">Habitliste erstellen📋</h1>
<div class="card bg-light p-5 mt-5">
<h1>Gewohnheitsliste erstellen📋</h1>
<form action="/habit-list" method="POST">
<div class="mb-3">
@ -21,5 +21,5 @@
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% endblock %}

View File

@ -1,8 +1,8 @@
{% extends 'layouts/main.html' %}
{% block content %}
<h1 class="mt-5">Habit erstellen📋</h1>
<div class="card bg-light p-5 mt-5">
<h1>Gewohnheit erstellen📋</h1>
<form action="/habit" method="POST">
<div class="mb-3">
@ -37,11 +37,11 @@
<option value="Jahr">Jahr</option>
</select>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
document.addEventListener('DOMContentLoaded', () => {
let selectedElement = document.getElementById('unit');
for (let option of selectedElement.options) {
if (option.value == '{{ unit }}') {
if (option.value === '{{ unit }}') {
option.selected = true;
break;
}
@ -61,13 +61,13 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
// Extracting the list-query from the URL
var listQuery = new URLSearchParams(window.location.search).get('list');
let listQuery = new URLSearchParams(window.location.search).get('list');
if ("{{ list_id }}" != "") {
if ("{{ list_id }}" !== "") {
listQuery = "{{ list_id }}";
// Add the list_id to the URL
var url = new URL(window.location.href);
const url = new URL(window.location.href);
url.searchParams.set('list', listQuery);
// window.history.pushState({}, '', url);
}
@ -78,5 +78,5 @@
</script>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% endblock %}

View File

@ -1,271 +1,39 @@
{% extends 'layouts/main.html' %}
{% block content %}
<h1>{{ title }}</h1>
<h3>{{ utc_dt }}</h3>
<style>
#heatmap {
display: grid;
grid-template-columns: repeat(7, 0fr); /* 7 Tage in einer Woche */
gap: 5px;
}
.day {
width: 50px;
height: 50px;
border: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<div class="row">
<div class="col-md-5 col-12">
<div id="heatmap"></div>
<div class = "row">
<div class="col">
<h1>
{% if (current_user.is_authenticated) %}<img class="avatar avatar-xl" src="{{user.profile_image}}" alt="no image"/>{% endif %}
{{ title }}
</h1>
<h3 id="current-time"></h3>
</div>
<script>
// Funktion zur Rückgabe des Montagsdatums
function getMonday(date) {
const day = date.getDay();
const diff = date.getDate() - day + (day === 0 ? -6 : 1); // Anpassung für Sonntag
return new Date(date.setDate(diff));
}
// Simulierte Aktivitätsdaten (ersetze dies durch deine echten Daten)
const activityData = [5, 3, 10, 5, 24, 2, 10, 47, 32, 45, 9, 5, 11, 39, 24, 2, 10, 47, 32, 45];
// Funktion zum Erstellen der Heatmap
function createHeatmap(data) {
const heatmapContainer = document.getElementById('heatmap');
// Aktuelles Datum des Montags
const days = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
for (let i = 0; i < 7; i++) {
const dayElement = document.createElement('div');
dayElement.classList.add('day');
dayElement.textContent = days[i];
heatmapContainer.appendChild(dayElement);
// currentDate.setDate(currentDate.getDate() + 1);
}
// Aktuelles Datum des Montags in der neuen linken Spalte
for (let i = 0; i < 7; i++) {
for (let j = 0; j < 7; j++) {
// console.log(i * 7 + j, data[i * 7 + j], Math.max(...data));
const opacity = data[i * 7 + j] / Math.max(...data); // Berechne die Opazität basierend auf Aktivitätsanzahl
if (data[i * 7 + j]) {
const dayElement = document.createElement('div');
dayElement.classList.add('day');
dayElement.style.backgroundColor = `rgba(0, 255, 0, ${opacity})`;
heatmapContainer.appendChild(dayElement);
} else {
const dayElement = document.createElement('div');
// dayElement.classList.add('day');
// dayElement.style.backgroundColor = `rgba(0, 255, 0, ${opacity})`;
heatmapContainer.appendChild(dayElement);
}
}
}
}
// Erstelle die Heatmap mit den simulierten Daten
createHeatmap(activityData);
</script>
<div class="col-md-7 col-12">
<div class="row mb-3">
<h2 class="col-9">Gewohnheiten</h2>
<a class="col-3 btn btn-primary" role="button" href="/habit-list">Neue Liste erstellen</a>
<div class="col-2">
<a href="https://anti-stress-team.de/blog/selbstmanagement/80-spannende-ideen-fuer-deinen-habit-tracker/" role="button" class="btn btn-discovery">Beliebte Habits</a>
</div>
</div>
{% for habit_list in habit_lists %}
<div class="d-md-flex gap-3">
<div class="row mb-3">
<h2 class="col-9">{{ habit_list.name }}</h2>
<a class="col-3 btn btn-primary" role="button" href="/habit?list={{ habit_list.id }}">Gewohnheit erstellen</a>
</div>
<ul class="task-list row">
{% for habit in habit_list.habits %}
<li class="row d-flex align-items-center mb-2" id="habit-{{ habit.id }}">
<div class="col-auto drag-handle" style="cursor: grab;">
<i class="bi bi-grip-vertical"></i>
</div>
<div class="col-auto">
<input {% if habit.checked %} checked {% endif %} type="checkbox" class="task-checkbox"
id="{{ habit.id }}"
onclick="sendPostRequest('{{ habit.id }}')">
</div>
<div class="col" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
{{ habit.name }}
</div>
<div class="col-6" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
{{ habit.note }}
</div>
<div class="col-2" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis">
{% if habit %}
5 🔥
{% if current_user.is_authenticated %}
{% include 'components/heatmap.html' %}
{% endif %}
</div>
{% if current_user.is_authenticated %}
{% include 'components/habit_lists.html' %}
{% endif %}
<button type="button" class="btn btn-xs btn-danger rounded-circle" data-bs-toggle="modal"
data-bs-target="#exampleModal" style="width: 40px; height: 40px"
onclick="setSelectedHabitId({{ habit.id }})">
<i class="bi bi-trash3"></i>
</button>
<div class="col-12">
<div class="progress" style="height: 2px; width: 90%">
<div class="progress-bar" id="progress-bar-{{ habit.id }}" role="progressbar"
style="width: {{ habit.percentage }}%; background-color: {% if habit.percentage >= 100 %} green {% else %} primary {% endif %}"
aria-valuenow="{{ habit.percentage }}" aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</li>
{% if current_user.is_authenticated %}
{% include 'components/delete_button.html' %}
{% endif %}
{% endfor %}
</ul>
{% if current_user.is_authenticated %}
{% include 'components/delete_list.html' %}
{% endif %}
{% endfor %}
</div>
</div>
<script>
var selectedHabitId = null;
<script src="../static/script/script-index.js"></script>
function setSelectedHabitId(habitId) {
selectedHabitId = habitId;
}
</script>
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Bestätige</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Möchtest du dieses Habit wirklich löschen?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary btn-danger" data-bs-dismiss="modal"
onclick="deleteHabit(selectedHabitId)">Löschen
</button>
</div>
</div>
</div>
</div>
<script>
function checkCompletionAndAnimate(habitId, percentage) {
var progressBar = document.getElementById("progress-bar-" + habitId);
var habitBlock = document.getElementById("habit-" + habitId);
if (percentage == 100) {
habitBlock.classList.add("animate-bounce");
setTimeout(function () {
habitBlock.classList.remove("animate-bounce");
}, 2000);
}
if (percentage >= 100) {
progressBar.style.backgroundColor = "green";
} else {
progressBar.style.backgroundColor = "";
habitBlock.classList.remove("animate-bounce");
}
}
function sendPostRequest(checkboxId) {
// Get the checkbox element using the provided ID
var checkbox = document.getElementById(checkboxId);
// console.log(checkbox);
// Get the habit id from the checkbox id attribute
var habitId = checkboxId;
// Make a POST request to /check with the habit id
axios.post('/check', {habitId: habitId}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
console.log(response.data);
// Set the percentage of the habit. percentage received as integer
var percentage = response.data.percentage;
var progressBar = document.getElementById("progress-bar-" + habitId);
progressBar.style.width = percentage + "%";
if (response.data.unchecked) {
checkCompletionAndAnimate(habitId, percentage);
}
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
function deleteHabit(habitId) {
// Make a POST request to /delete with the habit id
axios.post('/delete', {habitId: habitId}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
console.log(response.data);
// Remove the habit from the DOM
var habitElement = document.getElementById("habit-" + habitId);
habitElement.remove();
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
</script>
</div>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
var el = document.querySelector('.task-list');
Sortable.create(el, {
handle: '.drag-handle',
animation: 150,
onEnd: function (evt) {
var habitId = el.children[evt.newIndex].id.split('-')[1];
var oldIndex = evt.oldIndex;
var newIndex = evt.newIndex;
axios.post('/reorder', {habitId: habitId, oldIndex: oldIndex, newIndex: newIndex}, {
headers: {
'Content-Type': 'application/json'
}
}).then(function (response) {
// Handle the success response if needed
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
});
});
</script>
{% endblock %}

View File

@ -2,14 +2,17 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" >
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>{{ title }} - HabitTracker</title>
<link rel="icon" href="/static/icon.ico">
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="../../static/css/background.css">
<link rel="stylesheet" type="text/css" href="../../static/css/profile.css">
<link href="https://cdn.jsdelivr.net/npm/fastbootstrap@2.2.0/dist/css/fastbootstrap.min.css" rel="stylesheet" integrity="sha256-V6lu+OdYNKTKTsVFBuQsyIlDiRWiOmtC8VQ8Lzdm2i4=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="../../static/css/background.css" rel="stylesheet" type="text/css">
<link href="../../static/css/profile.css" rel="stylesheet" type="text/css">
<link href="../../static/css/heatmap.css" rel="stylesheet" type="text/css">
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/fastbootstrap@2.2.0/dist/css/fastbootstrap.min.css"
integrity="sha256-V6lu+OdYNKTKTsVFBuQsyIlDiRWiOmtC8VQ8Lzdm2i4=" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<!-- jQuery -->
@ -17,7 +20,9 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- Bootstrap JS (including Popper.js for Bootstrap 5) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<script crossorigin="anonymous"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
<!-- Axios Library-->
@ -30,29 +35,99 @@
<nav class="navbar navbar-expand-lg" style="background-color: #000000">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}" style="color: ivory">HabitTracker</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<button aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
data-bs-target="#navbarSupportedContent" data-bs-toggle="collapse" type="button">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link text-white {% if title == 'Home' %} active {% endif %}" aria-current="page" href="{{ url_for('index') }}">Home</a>
</li>
</ul>
<ul class="me-auto"></ul>
<ul class="navbar-nav mb-2 mb-lg-0">
{% if not current_user.is_authenticated %}
<li class="nav-item me-2">
<a class="btn text-white btn-primary" aria-current="page" href="{{ url_for('login') }}">Login</a>
<a aria-current="page" class="btn text-white btn-primary" href="{{ url_for('login') }}">Login</a>
</li>
<li class="nav-item">
<a class="btn btn-outline-secondary" aria-current="page" href="{{ url_for('signup') }}">Signup</a>
<a aria-current="page" class="btn btn-outline-secondary" href="{{ url_for('signup') }}">Signup</a>
</li>
{% else %}
<li class="nav-item me-2">
<a class="btn text-white btn-primary" aria-current="page" href="{{ url_for('profile') }}">Profil</a>
{% if notifications %}
<li class="nav-item me-4 mb-2 mb-lg-0">
<div class="dropdown">
<!--<button class="btn btn-default dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Page actions
</button>-->
<i class="bi bi-bell-fill dropdown-toggle" data-bs-toggle="dropdown"
style="color: white; cursor: pointer"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger">
{{ notifications|length }}
<span class="visually-hidden">unread messages</span>
</span>
<ul class="dropdown-menu" role="menu">
{% for notification in notifications %}
<li>
<div class="dropdown-item">
<div class="row">
<div class="col">
{{ notification.name }}
</div>
<div class="col">
<a class="accept-button" data-id="{{ notification.id }}" style="cursor: pointer"><i class="bi bi-check-circle-fill" style="color: green"></i></a>
</div>
<div class="col">
<a class="deny-button" data-id="{{ notification.id }}" style="cursor: pointer"><i class="bi bi-x-circle-fill" style="color: red"></i></a>
</div>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var acceptButtons = document.querySelectorAll('.accept-button');
acceptButtons.forEach(function(button) {
button.addEventListener('click', function() {
var notificationId = this.getAttribute('data-id');
console.log('Notification accepted:', notificationId);
axios.post('/accept-list', {list_id: notificationId}, {
headers: {
'Content-Type': 'application/json'
}
}).then(() => {
location.reload();
});
});
});
});
document.addEventListener('DOMContentLoaded', function() {
var acceptButtons = document.querySelectorAll('.deny-button');
acceptButtons.forEach(function(button) {
button.addEventListener('click', function() {
var notificationId = this.getAttribute('data-id');
console.log('Notification accepted:', notificationId);
axios.post('/deny-list', {list_id: notificationId}, {
headers: {
'Content-Type': 'application/json'
}
}).then(() => {
location.reload();
});
});
});
});
</script>
</li>
{% endif %}
<li class="nav-item me-2 mb-2 mb-lg-0">
<a aria-current="page" class="btn text-white btn-primary" href="{{ url_for('profile') }}">Profil</a>
</li>
<li class="nav-item me-2">
<a class="btn btn-primary" aria-current="page" href="{{ url_for('logout') }}">Logout</a>
<a aria-current="page" class="btn btn-primary" href="{{ url_for('logout') }}">Logout</a>
</li>
{% endif %}
</ul>

View File

@ -6,7 +6,7 @@
<h1 class="mb-4">Account Einstellungen👤</h1>
<!-- Account information fields -->
<div class="card mb-4">
<div class="card bg-light mb-4">
<div class="card-body d-flex">
<div>
<h5 class="card-title">Profilbild</h5>
@ -31,11 +31,20 @@
Bearbeiten
</button>
</div>
<form id="colorForm" action="/save_color" method="POST">
<div class="ml-5" style="margin-left: 50px;">
<h5 class="card-title">Heatmap Farbe</h5>
<div style="display: flex; align-items: center;">
<input type="color" name="color" class="form-control form-control-color" id="exampleColorInput" value="{{color}}" title="Choose your color" style="margin-right: 10px;">
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</div>
</form>
</div>
</div>
<!-- Password change fields -->
<div class="card mb-4">
<div class="card bg-light mb-4">
<div class="card-body">
<h5 class="card-title">Passwort ändern</h5>
<form id="editPasswordForm" action="/password" method="POST">
@ -54,8 +63,40 @@
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" autocomplete="new-password">
<div class="invalid-feedback" id="confirmPasswordFeedback"></div>
</div>
</form>
<button type="button" class="btn btn-primary" id="submitPasswordChange">Änderungen speichern</button>
</form>
<div style="margin-top: 30px;">
<h5 class="card-title">Konto löschen</h5>
<form id="deleteAccountForm" action="/delete_account" method="POST">
<button type="button" class="btn btn-danger" id="deleteAccountButton" data-toggle="modal" data-target="#confirmDeleteModal">
Konto dauerhaft löschen
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Delete Account Confirmation Modal -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" role="dialog" aria-labelledby="confirmDeleteModalLabel" aria-hidden="true">
<div class="modal-dialog mt-20" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmDeleteModalLabel">Konto löschen</h5>
<span class="close-icon" data-bs-dismiss="modal" aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24" height="24" viewBox="0 0 72 72" class="close-icon">
<path d="M 19 15 C 17.977 15 16.951875 15.390875 16.171875 16.171875 C 14.609875 17.733875 14.609875 20.266125 16.171875 21.828125 L 30.34375 36 L 16.171875 50.171875 C 14.609875 51.733875 14.609875 54.266125 16.171875 55.828125 C 16.951875 56.608125 17.977 57 19 57 C 20.023 57 21.048125 56.609125 21.828125 55.828125 L 36 41.65625 L 50.171875 55.828125 C 51.731875 57.390125 54.267125 57.390125 55.828125 55.828125 C 57.391125 54.265125 57.391125 51.734875 55.828125 50.171875 L 41.65625 36 L 55.828125 21.828125 C 57.390125 20.266125 57.390125 17.733875 55.828125 16.171875 C 54.268125 14.610875 51.731875 14.609875 50.171875 16.171875 L 36 30.34375 L 21.828125 16.171875 C 21.048125 15.391875 20.023 15 19 15 z" stroke="none"></path>
</svg>
</span>
</div>
<div class="modal-body">
Sind Sie sicher, dass Sie Ihr Konto dauerhaft löschen möchten?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-danger" id="confirmDeleteButton">Löschen</button>
</div>
</div>
</div>
</div>
@ -63,7 +104,7 @@
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-dialog mt-20" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Bearbeiten</h5>
@ -77,12 +118,12 @@
<form id="editForm" action="/profile" method="POST">
<div class="form-group">
<label for="newName">Neuer Name:</label>
<input type="text" class="form-control" id="newName" name="newName" value="{{name}}" autocomplete="username">
<input type="text" class="form-control" id="newName" name="newName" value="{{ name }}" autocomplete="username">
<div class="invalid-feedback" id="nameFeedback"></div>
</div>
<div class="form-group">
<label for="newEmail">Neue E-Mail:</label>
<input type="email" class="form-control" id="newEmail" name="newEmail" value="{{email}}" autocomplete="username">
<input type="email" class="form-control" id="newEmail" name="newEmail" value="{{ email }}" autocomplete="username">
<div class="invalid-feedback" id="emailFeedback"></div>
</div>
<div class="form-group">
@ -100,191 +141,7 @@
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Get elements
const profileImage = document.getElementById("profileImage");
const profileImageOverlay = document.getElementById("profileImageOverlay");
const profileImageInput = document.getElementById("profileImageInput");
const uploadForm = document.getElementById("uploadForm");
const editButton = document.getElementById("editButton");
const saveChangesButton = document.getElementById("saveChangesButton");
const editForm = document.getElementById("editForm");
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
const submitPasswordChangeButton = document.getElementById("submitPasswordChange");
// Open file input when profile image is clicked
profileImageOverlay.addEventListener("click", function() {
profileImageInput.click();
});
// Change profile image when a new file is selected
profileImageInput.addEventListener("change", function() {
const file = this.files[0];
const reader = new FileReader();
reader.onload = function(e) {
profileImage.src = e.target.result;
};
reader.readAsDataURL(file);
// Submit the form
uploadForm.submit();
});
// Add event listener to edit button to open modal
editButton.addEventListener("click", function() {
editModal.show();
document.getElementById("newName").value = "{{ name }}";
document.getElementById("newEmail").value = "{{ email }}";
document.getElementById("password").value = "";
});
// Add event listener to save changes button to submit form
saveChangesButton.addEventListener("click", function() {
// Perform client-side validation before submitting the form
validateForm()
.then(isValid => {
if (isValid) {
console.log("Validated 2");
editForm.submit();
}
})
.catch(error => {
// Handle validation error
console.log("Account Form validation failed", error);
});
});
// Function to perform client-side validation
async function validateForm() {
let isValid = true;
isValid = validateInput("newName", "nameFeedback", "Bitte geben Sie einen neuen Namen ein.") && isValid;
isValid = validateEmail("newEmail", "emailFeedback", "Bitte geben Sie eine gültige E-Mail-Adresse ein.") && isValid;
try {
const passwordValid = await validatePassword("password", "passwordFeedback", "Bitte geben Sie Ihr Passwort ein.");
isValid = passwordValid && isValid;
} catch (error) {
isValid = false;
}
return isValid;
}
// Function to validate input fields
function validateInput(inputId, feedbackId, errorMessage) {
const input = document.getElementById(inputId);
const feedback = document.getElementById(feedbackId);
if (!input.value.trim()) {
feedback.textContent = errorMessage;
input.classList.add("is-invalid");
return false;
} else {
feedback.textContent = "";
input.classList.remove("is-invalid");
return true;
}
}
// Function to validate email
function validateEmail(emailId, feedbackId, errorMessage) {
const input = document.getElementById(emailId);
const feedback = document.getElementById(feedbackId);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Regular expression for email validation
if (!input.value.trim()) {
feedback.textContent = "Bitte geben Sie eine neue E-Mail-Adresse ein.";
input.classList.add("is-invalid");
return false;
} else if (!emailRegex.test(input.value.trim())) {
feedback.textContent = errorMessage;
input.classList.add("is-invalid");
return false;
} else {
feedback.textContent = "";
input.classList.remove("is-invalid");
return true;
}
}
// Function to validate password
function validatePassword(passwordId, passwordFeedbackId, errorMessage) {
return new Promise((resolve, reject) => {
const passwordInput = document.getElementById(passwordId);
const passwordFeedback = document.getElementById(passwordFeedbackId);
const password = passwordInput.value.trim(); // Get the password entered by the user
if (!passwordInput.value.trim()) {
passwordFeedback.textContent = errorMessage;
passwordInput.classList.add("is-invalid");
reject(false);
} else {
axios.post('/check_password', { password: password })
.then(response => {
if (!response.data.valid) {
passwordFeedback.textContent = "Falsches Passwort.";
passwordInput.classList.add("is-invalid");
passwordInput.value = "";
reject(false);
} else {
passwordInput.classList.remove("is-invalid");
resolve(true);
}
})
.catch(error => {
console.error('Error checking password:', error);
reject(false);
});
}
});
}
submitPasswordChangeButton.addEventListener("click", function() {
// Perform client-side validation before submitting the form
validatePasswordChangeForm()
.then(isValid => {
if (isValid) {
document.getElementById("editPasswordForm").submit();
}
})
.catch(error => {
// Handle validation error
console.log("Password change form validation failed", error);
});
});
async function validatePasswordChangeForm() {
let isValid = true;
try {
const passwordValid = await validatePassword("oldPassword", "oldPasswordFeedback", "Bitte geben Sie Ihr altes Passwort ein.");
isValid = passwordValid && isValid;
} catch (error) {
isValid = false;
}
isValid = validateInput("newPassword", "newPasswordFeedback", "Bitte geben Sie Ihr neues Passwort ein.") && isValid;
isValid = validateInput("confirmPassword", "confirmPasswordFeedback", "Bitte bestätigen Sie Ihr neues Passwort.") && isValid;
// Check if new password matches confirm password
const newPassword = document.getElementById("newPassword").value.trim();
const confirmPassword = document.getElementById("confirmPassword").value.trim();
if (newPassword !== confirmPassword) {
document.getElementById("confirmPasswordFeedback").textContent = "Die Passwörter stimmen nicht überein.";
document.getElementById("confirmPassword").classList.add("is-invalid");
isValid = false;
} else {
document.getElementById("confirmPasswordFeedback").textContent = "";
document.getElementById("confirmPassword").classList.remove("is-invalid");
}
return isValid;
}
});
</script>
<script src="../static/script/script-profile.js"></script>
{% endblock %}

37
templates/users-edit.html Normal file
View File

@ -0,0 +1,37 @@
{% extends 'index.html' %}
{% block content %}
<div class="card bg-light p-5 mt-5">
<h1 class="mb-5">
{{ title }}
</h1>
{% for user in users %}
<form action="/user-delete" class="row" method="POST">
<div class="col">
<img src="{{ user.profile_image }}" class="avatar" alt=""/>
</div>
<div class="col">
{{ user.name }}
</div>
<div class="col">
{{ user.email }}
</div>
<div class="col">
<label>
<input hidden="hidden" name="habit_list_id" value="{{ habit_list.id }}">
</label>
<label>
<input hidden="hidden" name="habit_user_id" value="{{ user.id }}">
</label>
<button type="submit" class="btn btn-primary btn-danger">
Löschen
</button>
</div>
</form>
{% endfor %}
</div>
{% endblock %}

27
templates/users.html Normal file
View File

@ -0,0 +1,27 @@
{% extends 'layouts/main.html' %}
{% block content %}
<div class="card bg-light mt-4 p-5">
<h1>{{ habit_list.name }}: {{ title }}</h1>
<p>Lade Nutzer per ihrer E-Mail-Adresse ein</p>
<form action="/users" method="POST">
<div class="mb-3">
<label for="email" class="form-label">E-Mail</label>
<input type="text" placeholder="beispiel@cimeyclust.com"
class="form-control {% if errors.get('email') %} is-invalid {% endif %}" id="email" name="email"
value="{{email}}">
<div class="invalid-feedback">
{{ errors.get('email', '') }}
</div>
</div>
<label>
<input hidden="hidden" name="habit_list_id" value="{{ habit_list.id }}">
</label>
<!-- submit button -->
<button type="submit" class="btn btn-primary">Einladen</button>
</form>
</div>
{% endblock %}