Compare commits

...

140 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
Yapollon
684beb372f Profil upgrade and MORE
Very cool isn't it, totally didn't take a lot of time
2024-02-20 04:16:28 +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
Yapollon
6f363a104e Profile Pictures
YES YOU HEARD RIGHT!!

I ALSO DID THE HTML!

This is the most awesome most epic addition, i sold my kidney for this.
2024-02-18 04:53:15 +01:00
Yapollon
e5709775bb fixed renaming in app.py
he guess i missed some
2024-02-16 18:04:53 +01:00
Yapollon
759c7d277f Model Overhaul
I'm done, this is the now complete code for the models and all.

To make it short:
- made some renaming that was necessary, HabitTrackings -> HabitTracking because every goddamn class is singular and not plural
- made the deletion process everywhere the same (Habit Deletion now handles the HabitTracking deletion inside the model and not through Sqlite.py)
- Some much needed comments
- Some adjustments to the HabitLists, nothing big, plus addition of update

- Also as a question, why does the HabitList have a redundant habits attribute, for now i marked this issue in the code to not forget it
2024-02-16 17:57:49 +01:00
Yapollon
252c8825d1 updated URL
found a small issue
2024-02-16 17:30:27 +01:00
Yapollon
311d568525 New UML and ER
hope this is good, also i just lost all my changes from my previous work on this so I'm pretty depressed right now, anyway still got this,... great.
2024-02-16 17:10: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
Yapollon
4fcc20c880 Merge branch 'model' of https://repo.cimeyclust.com/CimeyClust/HabitTracker into model 2024-02-16 08:18:51 +01:00
Yapollon
18b63ba9a5 Create UML.dia.autosave 2024-02-16 08:18:08 +01:00
Yapollon
7600bd8f2c Fixing 2024-02-16 08:18:08 +01:00
Yapollon
6b492c5d49 Merge branch 'master' into model 2024-02-16 08:13:52 +01:00
Yapollon
91252a598b Create UML.dia.autosave 2024-02-16 08:11:25 +01:00
Yapollon
bad718289e Fixing 2024-02-16 08:11:22 +01:00
46 changed files with 2348 additions and 651 deletions

3
.gitignore vendored
View File

@ -165,3 +165,6 @@ cython_debug/
db/db.sqlite db/db.sqlite
/HabitTracker.iml /HabitTracker.iml
static/profile_images/*
!/static/profile_images/no_avatar/

BIN
ER.dia

Binary file not shown.

BIN
ER.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 97 KiB

BIN
ER_transparent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
UML.dia

Binary file not shown.

BIN
UML.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 43 KiB

717
app.py
View File

@ -1,25 +1,30 @@
import datetime import datetime
import hashlib import hashlib
import os
from flask import Flask, render_template, redirect, url_for, request 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 from flask_login import login_required, LoginManager, login_user, logout_user, current_user
from models.Habit import Habit from models.Habit import Habit
from models.HabitList import HabitList from models.HabitList import HabitList
from models.HabitTrackings import HabitTrackings from models.HabitTracking import HabitTracking
from models.User import User from models.User import User
from utils import anonymous_required from utils import anonymous_required
# Create a new Flask instance # Create a new Flask instance
app = Flask(__name__) app = Flask(__name__)
app.secret_key = 'PSSSSSHHHT!' 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 # Initialize the Flask-Login extension
login_manager = LoginManager() login_manager = LoginManager()
login_manager.login_view = 'login' login_manager.login_view = 'login'
login_manager.init_app(app) login_manager.init_app(app)
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return User.get(user_id) return User.get(user_id)
@ -30,18 +35,73 @@ def inject_user():
return dict(user=current_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') @app.route('/login')
@anonymous_required @anonymous_required
def login(): def login():
return render_template('auth/login.html', errors={}) return render_template('auth/login.html', errors={})
@app.route('/signup') @app.route('/signup')
@anonymous_required @anonymous_required
def signup(): def signup():
return render_template('auth/signup.html', errors={}) return render_template('auth/signup.html', errors={})
@app.route('/login', methods=['POST']) @app.route('/login', methods=['POST'])
def login_post(): def login_post():
email = request.form.get('email') email = request.form.get('email')
@ -75,7 +135,6 @@ def login_post():
# Redirect to login page # Redirect to login page
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/signup', methods=['POST']) @app.route('/signup', methods=['POST'])
def signup_post(): def signup_post():
email = request.form.get('email') email = request.form.get('email')
@ -91,6 +150,10 @@ def signup_post():
if not password: if not password:
errors['password'] = 'Das Passwort ist erforderlich.' 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: if errors:
return render_template( return render_template(
'auth/signup.html', 'auth/signup.html',
@ -107,7 +170,6 @@ def signup_post():
# Redirect to login page # Redirect to login page
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/logout') @app.route('/logout')
@login_required @login_required
def logout(): def logout():
@ -115,32 +177,11 @@ def logout():
logout_user() logout_user()
return redirect(url_for('index')) 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,
heatmap_values=current_user.get_heatmap(),
errors={},
)
########################## Habit ##########################
@app.route('/habit') @app.route('/habit')
@login_required @login_required
def habit_creation(): def habit_creation():
@ -151,7 +192,6 @@ def habit_creation():
errors={}, errors={},
) )
@app.route('/habit', methods=['POST']) @app.route('/habit', methods=['POST'])
@login_required @login_required
def habit_create(): def habit_create():
@ -194,6 +234,16 @@ def habit_create():
except ValueError: except ValueError:
errors['list_query'] = 'Die Anzahl muss eine Zahl sein.' 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: if errors:
return render_template( return render_template(
'habit.html', 'habit.html',
@ -219,113 +269,125 @@ def habit_create():
unit = 1 unit = 1
# Save habit to database # Save habit to database
habit = Habit.create(list_id, name, times, note, unit) Habit.create(list_id, name, times, note, unit)
# Back to index # Back to index
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/edit-habit')
@app.route('/habit-list')
@login_required @login_required
def habit_list_creation(): def edit_habit():
habit_id = int(request.args.get("habit"))
habit = Habit.get(habit_id)
units = ["Tag", "Woche", "Monat", "Jahr"]
return render_template( return render_template(
'habit-list.html', "edit-habit.html",
title='Erstelle eine Habitliste', title=habit.name,
errors={}, habit=habit.id,
name=habit.name,
note=habit.note,
times=habit.times,
unit=units[habit.unit],
errors={}
) )
@app.route('/edit-habit', methods=['POST'])
@app.route('/habit-list', methods=['POST'])
@login_required @login_required
def habit_list_create(): def edit_habit_change():
units = ["Tag", "Woche", "Monat", "Jahr"]
name = request.form.get('name') name = request.form.get('name')
description = request.form.get('description') 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 # Check for errors
errors = {} errors = {}
if not name: if not name:
errors['name'] = 'Der Name ist erforderlich.' errors['name'] = 'Der Name ist erforderlich.'
if not description: if not times:
errors['times'] = 'Die Anzahl ist erforderlich.'
if not note:
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: if errors:
return render_template( return render_template(
'habit-list.html', "edit-habit.html",
title='Erstelle eine Habitliste', title=habit.name,
name=name, habit=habit.id,
description=description, name=habit.name,
note=habit.note,
times=habit.times,
unit=units[habit.unit],
errors=errors 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 # Save habit to database
habit = HabitList.create(current_user.id, name, description) print(name, note, times, unit)
habit.name, habit.note, habit.times, habit.unit = name, note, times, unit
habit.update()
# Back to index # Back to index
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/profile')
@login_required
def profile():
return render_template(
"profile.html",
name=current_user.name,
email=current_user.email,
errors={}
)
@app.route('/profile', methods=['POST'])
@login_required
def profile_change():
newName = request.form.get('newName')
newEmail = request.form.get('newEmail')
newPassword = request.form.get('newPassword')
oldPassword = request.form.get('oldPassword')
# Check for errors
errors = {}
if not newName:
errors['newName'] = 'Der Name ist erforderlich.'
if not newEmail:
errors['newEmail'] = 'Die E-Mail Adresse ist erforderlich.'
if not oldPassword:
errors['oldPassword'] = 'Du musst dein aktuelles Passwort angeben.'
else:
if hashlib.sha256(oldPassword.encode()).hexdigest() != current_user.password:
errors['oldPassword'] = 'Das Passwort ist falsch.'
if errors:
return render_template(
"profile.html",
name=current_user.name,
email=current_user.email,
errors=errors
)
# Update user
current_user.name = newName
current_user.email = newEmail
if newPassword:
current_user.password = hashlib.sha256(newPassword.encode()).hexdigest()
current_user.update()
# Back to profile
return render_template(
"profile.html",
name=current_user.name,
email=current_user.email,
errors={}
)
@app.route('/check', methods=['POST']) @app.route('/check', methods=['POST'])
@login_required @login_required
def check_habit(): def check_habit():
habit_id = request.get_json()["habitId"] habit_id = request.get_json()["habitId"]
habit = Habit.get(habit_id) habit = Habit.get(habit_id)
if habit is None: if habit is None:
@ -345,17 +407,22 @@ def check_habit():
delete_tracking = tracking delete_tracking = tracking
if not delete_tracking: if not delete_tracking:
HabitTrackings.create(habit_id, 1) HabitTracking.create(habit_id)
habit.fill_statistics()
else: else:
delete_tracking.delete() delete_tracking.delete()
habit.reset_statistics()
# Update habit habit.load_statistics()
habit.fill_statistics() heatmap_values, day = current_user.get_heatmap()
return { return {
"habitId": habit_id, "habitId": habit_id,
"unchecked": not delete_tracking, "unchecked": not delete_tracking,
"percentage": habit.percentage, "percentage": habit.percentage,
"streak": habit.streak,
"heatmap": heatmap_values,
"day": day,
} }
@app.route('/delete', methods=['POST']) @app.route('/delete', methods=['POST'])
@ -375,11 +442,10 @@ def delete_habit():
habit.delete() habit.delete()
return {} return {}
@app.route('/reorder-habit', methods=['POST'])
@app.route('/reorder', methods=['POST'])
@login_required @login_required
def reorder_habits(): def reorder_habits():
new_index = request.get_json()["newIndex"]+1 new_index = request.get_json()["newIndex"] + 1
habit = Habit.get(request.get_json()["habitId"]) habit = Habit.get(request.get_json()["habitId"])
if habit is None: if habit is None:
@ -393,8 +459,431 @@ def reorder_habits():
habit.update_slot(new_index) habit.update_slot(new_index)
return {} return {}
###########################################################
######################## HabitList ########################
@app.route('/habit-list')
@login_required
def habit_list_creation():
return render_template(
'habit-list.html',
title='Erstelle eine Habitliste',
errors={},
)
@app.route('/habit-list', methods=['POST'])
@login_required
def habit_list_create():
name = request.form.get('name')
description = request.form.get('description')
# Check for errors
errors = {}
if not name:
errors['name'] = 'Der Name ist erforderlich.'
if not description:
description = ''
if errors:
return render_template(
'habit-list.html',
title='Erstelle eine Habitliste',
name=name,
description=description,
errors=errors
)
# Save habit to database
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():
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,
title="Profil",
)
@app.route('/profile', methods=['POST'])
@login_required
def profile_change():
newName = request.form.get('newName')
newEmail = request.form.get('newEmail')
# Update user
current_user.name = newName
current_user.email = newEmail
current_user.update()
# Back to profile
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,
)
@app.route('/check_password', methods=['POST'])
@login_required
def check_password():
# Get the password sent from the client
password = request.json.get('password')
if hashlib.sha256(password.encode()).hexdigest() == current_user.password:
return jsonify({"valid": True})
else:
return jsonify({"valid": False})
@app.route('/password', methods=['POST'])
@login_required
def password_change():
newPassword = request.form.get('newPassword')
# Update user
if newPassword:
current_user.password = hashlib.sha256(newPassword.encode()).hexdigest()
current_user.update()
# Back to profile
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,
)
def save_profile_image(image_file):
filename = image_file.filename
if '.' not in filename:
# Ensure the filename has an extension
raise ValueError("Invalid filename")
# Check if the file extension is allowed
allowed_extensions = {'jpg', 'jpeg', 'png', 'gif'}
file_extension = filename.rsplit('.', 1)[1].lower()
if file_extension not in allowed_extensions:
raise ValueError("Invalid file extension")
# Get the filename from the image path saved in the user
filename = os.path.basename(current_user.profile_image)
# Open the uploaded image
image = Image.open(image_file)
# 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)
# 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
# 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))
# 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.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']
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(
"profile.html",
name=current_user.name,
email=current_user.email,
profile_image_url=current_user.profile_image,
color=current_user.heatmap_color,
)
@app.route('/delete_account', methods=['POST'])
@login_required
def delete_account():
os.remove(current_user.profile_image)
current_user.delete()
return redirect(url_for('index'))
###########################################################
# Run the application # Run the application
if __name__ == '__main__': if __name__ == '__main__':
app.run(port=5000, debug=True) app.run(host="0.0.0.0", port=5000, debug=True)

View File

@ -1,6 +1,8 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import hashlib import hashlib
import sqlite3 import sqlite3
import os
import shutil
def con3(): def con3():
@ -8,18 +10,40 @@ def con3():
return conn return conn
### User.py ### ### User ###
def create_user(name: str, email: str, password: str): def create_user_profile_image(user_id):
script_directory = os.path.dirname(os.path.abspath(__file__))
source_path = os.path.join(script_directory, '../static/profile_images/no_avatar/user.jpg')
destination_directory = os.path.join(script_directory, '../static/profile_images/')
new_filename = f'user{user_id}-profile.jpg'
destination_path = os.path.join(destination_directory, new_filename)
shutil.copyfile(source_path, destination_path)
# Just save the part after static
static_index = destination_path.find('static')
relative_destination_path = destination_path[static_index:]
return relative_destination_path
def create_user(name: str, email: str, password: str, heatmap_color: str, profile_image: str = None):
password = hashlib.sha256(password.encode()).hexdigest() password = hashlib.sha256(password.encode()).hexdigest()
now = datetime.now().isoformat() now = datetime.now().isoformat()
query = (f"INSERT INTO users (name, email, password, created_at, updated_at) VALUES ('{name}', '{email}', " query = (f"INSERT INTO users (name, email, password, profile_image, heatmap_color, created_at, updated_at) VALUES "
f"'{password}', '{now}', '{now}');") f"('{name}', '{email}', '{password}', '{profile_image}', '{heatmap_color}', '{now}', '{now}');")
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
id = cursor.lastrowid
profile_image = create_user_profile_image(id)
query2 = f"UPDATE users SET profile_image = '{profile_image}' WHERE id = '{id}';"
cursor.execute(query2)
conn.commit() conn.commit()
conn.close() conn.close()
return cursor.lastrowid return id, profile_image
def get_user(id: int): def get_user(id: int):
@ -42,12 +66,15 @@ def get_user_by_email(email: str):
return user return user
def update_user(id: int, name: str, email: str, password: str = None): def update_user(id: int, name: str, email: str, password: str, profile_image: str, heatmap_color: str):
now = datetime.now().isoformat() now = datetime.now().isoformat()
if password: if password:
query = f"UPDATE users SET name = '{name}', email = '{email}', password = '{password}', updated_at = '{now}' WHERE id = {id};" 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: 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() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
@ -57,22 +84,20 @@ def update_user(id: int, name: str, email: str, password: str = None):
def delete_user(id: int): def delete_user(id: int):
query = f"DELETE FROM habit_lists WHERE (SELECT list_id FROM habit_users WHERE user_id = {id}) = id;" query = f"DELETE FROM users WHERE id = {id};"
query2 = f"DELETE FROM users WHERE id = {id};"
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
cursor.execute(query2)
conn.commit() conn.commit()
conn.close() conn.close()
return cursor.lastrowid return cursor.lastrowid
### Habit.py ### ### Habit ###
def create_habit(list_id: int, name: str, times: int, unit: int, slot: int, note: str | None=None): 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() now = datetime.now().isoformat()
query = (f"INSERT INTO habits (list_id, name, note, times, unit, slot, created_at, updated_at) VALUES ('{list_id}', " query = (f"INSERT INTO habits (list_id, name, note, times, unit, slot, checked, count, streak, created_at, updated_at) "
f"'{name}', '{note}', '{times}', '{unit}', '{slot}', '{now}', '{now}');") f"VALUES ('{list_id}', '{name}', '{note}', '{times}', '{unit}', '{slot}', '{checked}', '{count}', '{streak}', '{now}', '{now}');")
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
@ -103,7 +128,6 @@ def get_habits(list_id: int):
def get_heatmap_value(user_id: int, days: int): def get_heatmap_value(user_id: int, days: int):
date = (datetime.now() - timedelta(days=days)).date() date = (datetime.now() - timedelta(days=days)).date()
print(date)
# Uses JOINs to get all Habits # Uses JOINs to get all Habits
query = (f"SELECT habits.id FROM habits " query = (f"SELECT habits.id FROM habits "
@ -135,7 +159,7 @@ def get_heatmap_value(user_id: int, days: int):
return 0 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;" query = f"SELECT slot FROM habits WHERE list_id = {list_id} ORDER BY slot DESC LIMIT 1;"
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
@ -145,7 +169,7 @@ def get_next_slot(list_id: int):
return slot[0] + 1 if slot else 1 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;" query = f"SELECT id, slot FROM habits WHERE list_id = {list_id} ORDER BY slot;"
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
@ -155,7 +179,7 @@ def get_slots(list_id: int):
return slots return slots
def update_slot(id: int, slot: int): def habit_update_slot(id: int, slot: int):
now = datetime.now().isoformat() now = datetime.now().isoformat()
query = f"UPDATE habits SET slot = {slot}, updated_at = '{now}' WHERE id = {id};" query = f"UPDATE habits SET slot = {slot}, updated_at = '{now}' WHERE id = {id};"
conn = con3() conn = con3()
@ -168,7 +192,20 @@ def update_slot(id: int, slot: int):
def update_habit(id: int, name: str, note: str, times: int, unit: int): def update_habit(id: int, name: str, note: str, times: int, unit: int):
now = datetime.now().isoformat() now = datetime.now().isoformat()
query = f"UPDATE habits SET name = {name}, note = {note}, times = {times}, unit = {unit}, updated_at = '{now}' WHERE id = {id};" 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() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
@ -186,8 +223,8 @@ def delete_habit(id: int):
conn.close() conn.close()
### HabitTrackings.py ### ### HabitTracking ###
def create_habitTrackings(habit_id: int): def create_habitTracking(habit_id: int):
now = datetime.now().isoformat() now = datetime.now().isoformat()
query = f"INSERT INTO habit_trackings (habit_id, created_at) VALUES ('{habit_id}','{now}');" query = f"INSERT INTO habit_trackings (habit_id, created_at) VALUES ('{habit_id}','{now}');"
conn = con3() conn = con3()
@ -198,7 +235,7 @@ def create_habitTrackings(habit_id: int):
return cursor.lastrowid return cursor.lastrowid
def get_habitTrackings(id: int): def get_habitTracking(id: int):
query = f"SELECT * FROM habit_trackings WHERE id = {id};" query = f"SELECT * FROM habit_trackings WHERE id = {id};"
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
@ -208,7 +245,7 @@ def get_habitTrackings(id: int):
return habit_tracking return habit_tracking
def get_habitTrackings_by_habit_id(habit_id: int): def get_habitTrackings(habit_id: int):
query = f"SELECT * FROM habit_trackings WHERE habit_id = {habit_id};" query = f"SELECT * FROM habit_trackings WHERE habit_id = {habit_id};"
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
@ -218,7 +255,7 @@ def get_habitTrackings_by_habit_id(habit_id: int):
return habit_trackings return habit_trackings
def delete_habitTrackings(id: int): def delete_habitTracking(id: int):
query = f"DELETE FROM habit_trackings WHERE id = {id};" query = f"DELETE FROM habit_trackings WHERE id = {id};"
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
@ -227,16 +264,16 @@ def delete_habitTrackings(id: int):
conn.close() conn.close()
### HabitList.py ### ### 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() now = datetime.now().isoformat()
query = (f"INSERT INTO habit_lists (name, description, created_at, updated_at) " query = (f"INSERT INTO habit_lists (name, description, slot, created_at, updated_at) "
f"VALUES ('{name}', '{description}', '{now}', '{now}');") f"VALUES ('{name}', '{description}', '{slot}', '{now}', '{now}');")
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
query2 = (f"INSERT INTO habit_users (user_id, list_id, created_at, updated_at)" query2 = (f"INSERT INTO habit_users (user_id, list_id, created_at, updated_at, accepted)"
f" VALUES ('{user_id}', '{cursor.lastrowid}', '{now}', '{now}');") f" VALUES ('{user_id}', '{cursor.lastrowid}', '{now}', '{now}', 1);")
cursor.execute(query2) cursor.execute(query2)
conn.commit() conn.commit()
conn.close() conn.close()
@ -253,8 +290,41 @@ def get_habitList(id: int):
return habit_list 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): 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};") f"WHERE habit_users.user_id = {user_id};")
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
@ -264,6 +334,57 @@ def get_habitLists(user_id: int):
return habit_lists 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};")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
users = cursor.fetchall()
conn.close()
return users
def add_user(list_id: int, user_id: int):
now = datetime.now().isoformat()
query = (f"INSERT INTO habit_users (user_id, list_id, created_at, updated_at)"
f" VALUES ('{user_id}', '{list_id}', '{now}', '{now}');")
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
conn.commit()
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_users WHERE user_id = {user_id} AND list_id = {list_id};"
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
conn.commit()
conn.close()
def update_habitList(id: int, name: str, description: str): def update_habitList(id: int, name: str, description: str):
now = datetime.now().isoformat() now = datetime.now().isoformat()
query = f"UPDATE habit_lists SET name = {name}, description = {description}, updated_at = '{now}' WHERE id = {id};" query = f"UPDATE habit_lists SET name = {name}, description = {description}, updated_at = '{now}' WHERE id = {id};"
@ -284,16 +405,6 @@ def delete_habitList(id: int):
conn.close() conn.close()
def get_users(list_id: int):
query = f"SELECT users.* FROM users JOIN habit_users ON users.id = habit_users.user_id WHERE habit_users.list_id = {list_id};"
conn = con3()
cursor = conn.cursor()
cursor.execute(query)
users = cursor.fetchall()
conn.close()
return users
if __name__ == "__main__": if __name__ == "__main__":
habits = get_habits(1) habits = get_habits(1)
for habit in habits: for habit in habits:

View File

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

View File

@ -0,0 +1,10 @@
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,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);

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,17 +1,18 @@
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from datetime import timedelta
from models.HabitTrackings import HabitTrackings from models.HabitTracking import HabitTracking
from db.SQLiteClient import update_slot, create_habit, get_habit, delete_habit, get_next_slot, \ from db.SQLiteClient import (create_habit, get_habit, update_habit, delete_habit, habit_get_next_slot, habit_get_slots,
get_habitTrackings_by_habit_id, get_slots, update_habit, get_habitList habit_update_slot, get_habitTrackings, get_habitList, update_habit_statistics)
# Unit wird als Integers wie folgt gemessen: # unit will be represented by integers like this:
# 0: Tag # 0: day
# 1: Woche (Default) # 1: week (default)
# 2: Monat # 2: month
# 3: Jahr # 3: year
@dataclass @dataclass
class Habit: class Habit:
@ -22,93 +23,120 @@ class Habit:
times: int times: int
unit: int unit: int
slot: int slot: int
checked: bool
count: int
streak: int
percentage: int = 0 percentage: int = 0
def __post_init__(self): def __post_init__(self):
self.fill_statistics() self.load_statistics()
@staticmethod @staticmethod
def create(list_id: int, name: str, times: int, note: str | None = None, unit: int | None = 1): 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 = get_next_slot(list_id) slot = habit_get_next_slot(list_id)
id = create_habit(list_id, name, times, unit, slot, note) id = create_habit(list_id, name, note, times, unit, slot, checked, count, streak)
return Habit(id, list_id, name, note, times, unit, slot) return Habit(id, list_id, name, note, times, unit, slot, checked, count, streak)
@staticmethod @staticmethod
def get(id: int): def get(id: int):
habit = get_habit(id) habit = get_habit(id)
habit = 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
return habit
def update(self, name: str=None, note: str=None, times: int=None, unit: int=None): # Updates: name, note, times, unit
update_habit(self.id, name, note, times, unit) def update(self):
if name is not None: update_habit(self.id, self.name, self.note, self.times, self.unit)
self.name = name
if note is not None:
self.note = note
if times is not None:
self.times = times
if unit is not None:
self.unit = unit
# Updates the slot and reorders the HabitList accordingly
def update_slot(self, new_slot: int): def update_slot(self, new_slot: int):
slots = get_slots(self.list_id) # Fetches a list with the following structure [(id, slot), (id, slot), ...]
if new_slot > self.slot: slots = habit_get_slots(self.list_id)
slots = slots[self.slot:new_slot]
for slot in slots:
update_slot(slot[0], slot[1]-1)
if new_slot < self.slot:
slots = slots[new_slot-1:self.slot-1]
for slot in slots:
update_slot(slot[0], slot[1]+1)
update_slot(self.id, new_slot)
def delete(self): # Splits the list depending on whether the new slot is higher or lower than the current one
slots = get_slots(self.list_id)[self.slot+1:] 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: 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:
habit_update_slot(slot[0], slot[1]+1)
# Update the slot of the current habit
habit_update_slot(self.id, new_slot)
# Deletes the Habit
def delete(self):
# Reorders the slots
slots = habit_get_slots(self.list_id)[self.slot+1:]
for slot in slots:
habit_update_slot(slot[0], slot[1] - 1)
# Deletes all track-records associated with the Habit
trackings = self.get_habitTrackings()
for tracking in trackings:
tracking.delete()
# Deletes the current Habit
delete_habit(self.id) delete_habit(self.id)
def get_habitTrackings(self) -> list[HabitTrackings]:
# Returns all track-records for a Habit
def get_habitTrackings(self) -> list:
trackings = [] trackings = []
for rawTracking in get_habitTrackings_by_habit_id(self.id): for rawTracking in get_habitTrackings(self.id):
trackings.append(HabitTrackings(rawTracking[0], rawTracking[1], trackings.append(HabitTracking(rawTracking[0], rawTracking[1],
datetime.strptime(rawTracking[2], "%Y-%m-%dT%H:%M:%S.%f"))) datetime.strptime(rawTracking[2], "%Y-%m-%dT%H:%M:%S.%f")))
return trackings return trackings
def fill_statistics(self):
count = 0 # Returns the HabitList in which the Habit is located
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], raw_habitLists[3]) if raw_habitLists else None
# 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 self.checked = False
for tracking in self.get_habitTrackings(): if not yesterday in tracking_dates:
if tracking.created_at.date() == datetime.today().date(): self.streak = 0
self.checked = True update_habit_statistics(self.id, self.count, self.count, self.streak)
# day # Reset count based on time unit
if self.unit == 0: if self.unit == 0:
if tracking.created_at.date() == datetime.today().date(): self.count = 0
count += 1 elif self.unit == 1 and today.weekday() == 0:
# week self.count = 0
elif self.unit == 1: elif self.unit == 2 and today.day == 1:
if tracking.created_at.isocalendar()[1] == datetime.today().isocalendar()[1]: self.count = 0
count += 1 elif self.unit == 3 and today.month == 1 and today.day == 1:
# month self.count = 0
elif self.unit == 2:
if tracking.created_at.month == datetime.today().month: self.percentage = int(self.count / self.times * 100)
count += 1
# year # Saves the progress count and streak
elif self.unit == 3: def fill_statistics(self):
if tracking.created_at.year == datetime.today().year: self.checked = True
count += 1 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)
self.percentage = int(count / self.times * 100)
def to_json(self): def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
def habit_list(self):
from models.HabitList import HabitList
raw_habitLists = get_habitList(self.list_id)
return HabitList(raw_habitLists[0], raw_habitLists[1], raw_habitLists[2],
datetime.strptime(raw_habitLists[3], "%Y-%m-%dT%H:%M:%S.%f"),
datetime.strptime(raw_habitLists[4], "%Y-%m-%dT%H:%M:%S.%f")) \
if raw_habitLists else None

View File

@ -1,9 +1,10 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date, datetime
from db.SQLiteClient import delete_habitList, create_habitList, get_habitList, get_habits, get_users
from models.Habit import Habit from models.Habit import Habit
from models.User import User 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, habitList_get_next_slot, habitList_get_slots,
habitList_update_slot)
@dataclass @dataclass
@ -11,37 +12,89 @@ class HabitList:
id: int id: int
name: str name: str
description: str description: str
created_at: date slot: int
updated_at: date
habits: list = None habits: list = None
@staticmethod @staticmethod
def create(user_id: int, name: str, description: str): def create(user_id: int, name: str, description: str):
id = create_habitList(user_id, name, description) slot = habitList_get_next_slot(user_id)
return HabitList(id, name, description, datetime.now(), datetime.now()) id = create_habitList(user_id, name, description, slot)
return HabitList(id, name, description, slot)
@staticmethod @staticmethod
def get(id: int): def get(id: int):
habitList = get_habitList(id) habitList = get_habitList(id)
return HabitList(habitList[0], habitList[1], habitList[2], datetime.strptime(habitList[3], "%Y-%m-%dT%H:%M:%S.%f"), datetime.strptime(habitList[4], "%Y-%m-%dT%H:%M:%S.%f")) if habitList else None return HabitList(habitList[0], habitList[1], habitList[2], habitList[3]) if habitList else None
def delete(self):
# Updates: name, description
def update(self):
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):
# 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) delete_habitList(self.id)
def get_habits(self):
# Returns the Habits connected with the HabitList
def get_habits(self) -> list:
raw_habits = get_habits(self.id) raw_habits = get_habits(self.id)
habits = [] habits = []
for habit in raw_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) habits.append(habit)
return habits return habits
def get_users(self):
# Returns the Users connected with the HabitList
def get_users(self) -> list:
raw_users = get_users(self.id) raw_users = get_users(self.id)
users = [] users = []
for user in raw_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) users.append(user)
return users return users
# Adds a User by email to the HabitList
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):
remove_user(self.id, user_id)

26
models/HabitTracking.py Normal file
View File

@ -0,0 +1,26 @@
from datetime import date, datetime
from dataclasses import dataclass
from db.SQLiteClient import create_habitTracking, get_habitTracking, delete_habitTracking
@dataclass
class HabitTracking:
id: int
habit_id: int
created_at: date
@staticmethod
def create(habit_id: int):
id = create_habitTracking(habit_id)
return HabitTracking(id, habit_id, datetime.now())
@staticmethod
def get(id: int):
habitTrackings = get_habitTracking(id)
return HabitTracking(habitTrackings[0], habitTrackings[1],
datetime.strptime(habitTrackings[2], "%Y-%m-%dT%H:%M:%S.%f")) \
if habitTrackings else None
# Deletes the HabitTracking
def delete(self):
delete_habitTracking(self.id)

View File

@ -1,24 +0,0 @@
from dataclasses import dataclass
from datetime import date, datetime
from db.SQLiteClient import create_habitTrackings, get_habitTrackings, delete_habitTrackings
@dataclass
class HabitTrackings:
id: int
habit_id: int
created_at: date
@staticmethod
def create(habit_id: int, times: int):
id = create_habitTrackings(habit_id)
return HabitTrackings(id, habit_id, datetime.now())
@staticmethod
def get(id: int):
habitTrackings = get_habitTrackings(id)
return HabitTrackings(habitTrackings[0], habitTrackings[1], datetime.strptime(habitTrackings[2], "%Y-%m-%dT%H:%M:%S.%f")) if habitTrackings else None
def delete(self):
delete_habitTrackings(self.id)

View File

@ -1,52 +1,95 @@
from datetime import datetime from datetime import datetime
from flask_login import UserMixin from flask_login import UserMixin
from db.SQLiteClient import create_user, get_user, get_user_by_email, delete_user, update_user, \ 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): class User(UserMixin):
def __init__(self, id: int, name: str, email: str, password: str | None = 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.id = id
self.name = name self.name = name
self.email = email self.email = email
self.password = password self.password = password
self.profile_image = profile_image
self.heatmap_color = heatmap_color
@staticmethod @staticmethod
def create(name: str, email: str, password: str): def create(name: str, email: str, password: str):
id = create_user(name, email, password) heatmap_color = "#00FF00"
return User(id, name, email) 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 @staticmethod
def get(id: int): def get(id: int):
user = get_user(id) user = get_user(id)
return User(user[0], user[1], user[2], user[3]) if user else None return User(user[0], user[1], user[2], user[3], user[4], user[5]) if user else None
@staticmethod @staticmethod
def get_by_email(email: str): def get_by_email(email: str):
user = get_user_by_email(email) user = get_user_by_email(email)
return User(user[0], user[1], user[2], user[3]) 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): 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): def delete(self):
# calls the deletion of the users habitLists
habitLists = self.get_habitLists()
for habitList in habitLists:
habitList.delete(self.id)
# deletes the user
delete_user(self.id) delete_user(self.id)
def get_habitLists(self):
# Returns all HabitLists connected with the user
def get_habitLists(self) -> list:
from models.HabitList import HabitList from models.HabitList import HabitList
raw_habitLists = get_habitLists(self.id) raw_habitLists = get_habitLists(self.id)
habitLists = [] habitLists = []
for habitList in raw_habitLists: for habitList in raw_habitLists:
habitList = HabitList(habitList[0], habitList[1], habitList[2], datetime.strptime(habitList[3], "%Y-%m-%dT%H:%M:%S.%f"), datetime.strptime(habitList[4], "%Y-%m-%dT%H:%M:%S.%f")) accepted = habitList[6]
habitList = HabitList(habitList[0], habitList[1], habitList[2], habitList[3])
if accepted == 1:
habitLists.append(habitList) habitLists.append(habitList)
return habitLists return habitLists
def get_heatmap(self):
heatmap = [] def get_unaccepted_habitLists(self) -> list:
for day in range(0, 28): 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) -> 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) value = get_heatmap_value(self.id, day)
heatmap.append(value) 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;
}
}

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

@ -0,0 +1,73 @@
/* Profile image */
.profile-image-container {
position: relative;
width: 150px; /* Adjust the size as needed */
height: 150px; /* Adjust the size as needed */
}
.profile-image {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.profile-image-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Grey overlay */
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 16px;
opacity: 0; /* Initially hidden */
transition: opacity 0.3s ease;
cursor: pointer;
}
.profile-image-overlay:hover {
opacity: 1; /* Show overlay on hover */
}
.profile-image-overlay span {
/* Center the text and make it bold */
text-align: center;
font-weight: bold;
}
.profile-image-overlay:hover span {
/* Style the text when hovering */
color: white;
}
/* Edit-Modal Close Button */
.close-icon {
fill: #aaa;
cursor: pointer;
}
.close-icon:hover {
fill: #777; /* Change the color to whatever you'd like on hover */
}
.fade-out {
-webkit-animation: fadeOut 3s forwards;
animation: fadeOut 3s forwards;
}
@keyframes fadeOut {
0% { opacity: 1; }
100% { opacity: 0; display: none; }
}
@-webkit-keyframes fadeOut {
0% { opacity: 1; }
100% { opacity: 0; display: none; }
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 30" version="1.1" x="0px" y="0px"><title>324324</title><desc>Created with Sketch.</desc><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path d="M12.7296635,21.1661847 C12.7296635,21.6239997 12.3585312,21.995132 11.9007162,21.995132 C11.4429012,21.995132 11.0717688,21.6239997 11.0717688,21.1661847 L11.0717688,20.0609215 C11.0717688,19.6031065 11.4429012,19.2319741 11.9007162,19.2319741 C12.3585312,19.2319741 12.7296635,19.6031065 12.7296635,20.0609215 L12.7296635,21.1661847 Z" fill="#000000" fill-rule="nonzero"/><path d="M8.30861091,20.5510749 C8.30861091,21.0088899 7.93747853,21.3800223 7.47966354,21.3800223 C7.02184855,21.3800223 6.65071617,21.0088899 6.65071617,20.5510749 L6.65071617,19.4458117 C6.65071617,18.9879968 7.02184855,18.6168644 7.47966354,18.6168644 C7.93747853,18.6168644 8.30861091,18.9879968 8.30861091,19.4458117 L8.30861091,20.5510749 Z" fill="#000000" fill-rule="nonzero" transform="translate(7.479664, 19.998443) rotate(20.000000) translate(-7.479664, -19.998443) "/><path d="M17.1507162,20.5510749 C17.1507162,21.0088899 16.7795838,21.3800223 16.3217688,21.3800223 C15.8639538,21.3800223 15.4928214,21.0088899 15.4928214,20.5510749 L15.4928214,19.4458117 C15.4928214,18.9879968 15.8639538,18.6168644 16.3217688,18.6168644 C16.7795838,18.6168644 17.1507162,18.9879968 17.1507162,19.4458117 L17.1507162,20.5510749 Z" fill="#000000" fill-rule="nonzero" transform="translate(16.321769, 19.998443) scale(-1, 1) rotate(20.000000) translate(-16.321769, -19.998443) "/><path d="M21.0631579,18.5616012 C21.0631579,19.0194162 20.6920255,19.3905486 20.2342105,19.3905486 C19.7763955,19.3905486 19.4052632,19.0194162 19.4052632,18.5616012 L19.4052632,17.4563381 C19.4052632,16.9985231 19.7763955,16.6273907 20.2342105,16.6273907 C20.6920255,16.6273907 21.0631579,16.9985231 21.0631579,17.4563381 L21.0631579,18.5616012 Z" fill="#000000" fill-rule="nonzero" transform="translate(20.234211, 18.008970) scale(-1, 1) rotate(35.000000) translate(-20.234211, -18.008970) "/><path d="M4.26315789,18.5616012 C4.26315789,19.0194162 3.89202552,19.3905486 3.43421053,19.3905486 C2.97639554,19.3905486 2.60526316,19.0194162 2.60526316,18.5616012 L2.60526316,17.4563381 C2.60526316,16.9985231 2.97639554,16.6273907 3.43421053,16.6273907 C3.89202552,16.6273907 4.26315789,16.9985231 4.26315789,17.4563381 L4.26315789,18.5616012 Z" fill="#000000" fill-rule="nonzero" transform="translate(3.434211, 18.008970) rotate(35.000000) translate(-3.434211, -18.008970) "/><path d="M2.18356954,13.4118965 C3.65553254,16.0777919 8.10236709,18.625 12,18.625 C15.8799564,18.625 20.3024429,16.1029471 21.7976771,13.4455402 C22.0007957,13.0845475 21.8728134,12.6272449 21.5118207,12.4241264 C21.150828,12.2210078 20.6935255,12.3489901 20.4904069,12.7099828 C19.2639874,14.8896385 15.3442219,17.125 12,17.125 C8.64124122,17.125 4.70099679,14.8679737 3.49670178,12.686856 C3.29648738,12.3242446 2.84022687,12.1925958 2.47761541,12.3928102 C2.11500396,12.5930246 1.98335513,13.0492851 2.18356954,13.4118965 Z" fill="#000000" fill-rule="nonzero"/></g><text x="0" y="39" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Sonya Nikolaeva</text><text x="0" y="44" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 30" version="1.1" x="0px" y="0px"><title>123123</title><desc>Created with Sketch.</desc><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><path d="M12,18.625 C16.6985283,18.625 22.25,14.8199617 22.25,11.9375 C22.25,9.05503835 16.6985283,5.25 12,5.25 C7.30147173,5.25 1.75,9.05503835 1.75,11.9375 C1.75,14.8199617 7.30147173,18.625 12,18.625 Z M12,17.125 C8.0358394,17.125 3.25,13.8447343 3.25,11.9375 C3.25,10.0302657 8.0358394,6.75 12,6.75 C15.9641606,6.75 20.75,10.0302657 20.75,11.9375 C20.75,13.8447343 15.9641606,17.125 12,17.125 Z" fill="#000000" fill-rule="nonzero"/><path d="M12,16.25 C14.381728,16.25 16.3125,14.319228 16.3125,11.9375 C16.3125,9.55577202 14.381728,7.625 12,7.625 C9.61827202,7.625 7.6875,9.55577202 7.6875,11.9375 C7.6875,14.319228 9.61827202,16.25 12,16.25 Z M12,14.75 C10.4466991,14.75 9.1875,13.4908009 9.1875,11.9375 C9.1875,10.3841991 10.4466991,9.125 12,9.125 C13.5533009,9.125 14.8125,10.3841991 14.8125,11.9375 C14.8125,13.4908009 13.5533009,14.75 12,14.75 Z" fill="#000000" fill-rule="nonzero"/></g><text x="0" y="39" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Sonya Nikolaeva</text><text x="0" y="44" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 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' %} {% extends 'layouts/main.html' %}
{% block content %} {% block content %}
<div class="card"> <div class="card bg-light mt-4">
<h5 class="card-header">Login</h5> <h1 class="card-header">Login</h1>
<div class="card-body column"> <div class="card-body column">
<form method="POST" action="/login"> <form method="POST" action="/login">
<div class="mb-3 row"> <div class="mb-3 row">

View File

@ -1,26 +1,30 @@
{% extends 'layouts/main.html' %} {% extends 'layouts/main.html' %}
{% block content %} {% block content %}
<div class="column"> <div class="card bg-light mt-4 p-5">
<h3>Registrieren</h3> <div class="column">
<h1>Registrieren</h1>
<form method="POST" action="/signup"> <form method="POST" action="/signup">
<div class="mb-3"> <div class="mb-3">
<label for="email" class="form-label">Email-Adresse</label> <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"> <div class="invalid-feedback">
{{ errors.get('email', '') }} {{ errors.get('email', '') }}
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Name</label> <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"> <div class="invalid-feedback">
{{ errors.get('name', '') }} {{ errors.get('name', '') }}
</div> </div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Passwort</label> <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"> <div class="invalid-feedback">
{{ errors.get('password', '') }} {{ errors.get('password', '') }}
</div> </div>
@ -29,5 +33,6 @@
<button type="submit" class="btn btn-primary mb-3">Registrieren</button> <button type="submit" class="btn btn-primary mb-3">Registrieren</button>
</div> </div>
</form> </form>
</div>
</div> </div>
{% endblock %} {% 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' %} {% extends 'layouts/main.html' %}
{% block content %} {% block content %}
<div class="card bg-light p-5 mt-5">
<h1 class="mt-5">Habitliste erstellen📋</h1> <h1>Gewohnheitsliste erstellen📋</h1>
<form action="/habit-list" method="POST"> <form action="/habit-list" method="POST">
<div class="mb-3"> <div class="mb-3">
@ -21,5 +21,5 @@
</div> </div>
<button type="submit" class="btn btn-primary">Submit</button> <button type="submit" class="btn btn-primary">Submit</button>
</form> </form>
</div>
{% endblock %} {% endblock %}

View File

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

View File

@ -1,302 +1,39 @@
{% extends 'layouts/main.html' %} {% extends 'layouts/main.html' %}
{% block content %} {% block content %}
<h1>{{ title }}</h1> <div class = "row">
<h3>{{ utc_dt }}</h3> <div class="col">
<h1>
{% if (current_user.is_authenticated) %}<img class="avatar avatar-xl" src="{{user.profile_image}}" alt="no image"/>{% endif %}
<style> {{ title }}
#heatmap { </h1>
display: grid; <h3 id="current-time"></h3>
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;
}
</style>
<div class="d-flex flex-column gap-5">
<div class="d-flex gap-3">
<div class="flex-md-fill col-md-4 col-12 card bg-light mb-6">
<div class="card-body">
<h5 class="card-title">Heatmap</h5>
<div id="heatmap"></div>
</div> </div>
</div> <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>
<script>
// Simulierte Aktivitätsdaten (ersetze dies durch deine echten Daten)
const activityData = {{ heatmap_values }};
// 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 < 4; j++) {
// console.log(i * 7 + j, data[i * 7 + j], Math.max(...data));
const opacity = data[i * 7 + j] / (Math.max(...data) <= 0 ? 1 : 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);
}
}
}
var left = 7 - (new Date()).getDay();
}
// Erstelle die Heatmap mit den simulierten Daten
createHeatmap(activityData);
</script>
<div class="flex-fill col-md-8 col-12 card bg-light p-6 mb-6">
<div class="row mb-3">
<h2 class="col-9">Gewohnheiten</h2>
<a class="col-3 btn btn-primary p" role="button" href="/habit-list">Neue Liste erstellen</a>
</div>
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
{% for habit_list in habit_lists %}
<li class="nav-item">
{% if habit_list == habit_lists[0] %}
<a class="nav-link active" aria-current="true">{{ habit_list.name }}</a>
{% else %}
<a class="nav-link">{{ habit_list.name }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<div class="card-body text-center">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">
With supporting text below as a natural lead-in to additional content.
</p>
</div>
{% for habit_list in habit_lists %}
<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-5 text-black text-opacity-50"
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 %}
{% else %}
5 🔥
{% endif %}
</div>
<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>
{% endfor %}
</ul>
{% endfor %}
</div>
<script>
var selectedHabitId = null;
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) {
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");
}
}
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 + "%";
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> </div>
</div> </div>
<script> <div class="d-md-flex gap-3">
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}, { {% if current_user.is_authenticated %}
headers: { {% include 'components/heatmap.html' %}
'Content-Type': 'application/json' {% endif %}
}
}).then(function (response) {
// Handle the success response if needed
}).catch(function (error) {
// Handle the error if needed
console.error('Error:', error);
});
}
});
});
</script>
{% if current_user.is_authenticated %}
{% include 'components/habit_lists.html' %}
{% endif %}
{% if current_user.is_authenticated %}
{% include 'components/delete_button.html' %}
{% endif %}
{% if current_user.is_authenticated %}
{% include 'components/delete_list.html' %}
{% endif %}
</div>
<script src="../static/script/script-index.js"></script>
{% endblock %} {% endblock %}

View File

@ -2,45 +2,132 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <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> <title>{{ title }} - HabitTracker</title>
<link rel="icon" href="/static/icon.ico">
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="/static/main.css"> <link href="../../static/css/background.css" rel="stylesheet" type="text/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 href="../../static/css/profile.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.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 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<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 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--> <!-- Axios Library-->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head> </head>
<body style="background-color: White"> <body style="background-color: White">
<nav class="navbar navbar-expand-lg" style="background-color: #000000"> <nav class="navbar navbar-expand-lg" style="background-color: #000000">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}" style="color: ivory">HabitTracker</a> <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> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="me-auto"></ul>
<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="navbar-nav mb-2 mb-lg-0"> <ul class="navbar-nav mb-2 mb-lg-0">
{% if not current_user.is_authenticated %} {% if not current_user.is_authenticated %}
<li class="nav-item me-2"> <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>
<li class="nav-item"> <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> </li>
{% else %} {% else %}
<li class="nav-item me-2"> {% if notifications %}
<a class="btn text-white btn-primary" aria-current="page" href="{{ url_for('profile') }}">Profil</a> <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>
<li class="nav-item me-2"> <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> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -51,9 +138,6 @@
<div class="container mt-3 pb-3"> <div class="container mt-3 pb-3">
{% block content %} {% endblock %} {% block content %} {% endblock %}
<!-- Bootstrap JS -->
<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 src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
</div> </div>
</body> </body>
</html> </html>

View File

@ -2,45 +2,146 @@
{% block content %} {% block content %}
<h1 class="mt-5">Account Einstellungen👤</h1> <div class="container mt-5">
<h1 class="mb-4">Account Einstellungen👤</h1>
<!-- Account information fields -->
<form action="/profile" method="POST"> <div class="card bg-light mb-4">
<div class="form-group mb-3"> <div class="card-body d-flex">
<label for="newName">Neuer Name:</label> <div>
<input type="text" class="form-control {% if errors.get('newName') %} is-invalid {% endif %}" id="newName" name="newName" value="{{name}}"> <h5 class="card-title">Profilbild</h5>
<div class="invalid-feedback"> <div class="mb-3 profile-image-container" id="profileImageContainer">
{{ errors.get('newName', '') }} <img src="{{ profile_image_url }}" alt="Profile Image" class="profile-image" id="profileImage">
</div> <div class="profile-image-overlay" id="profileImageOverlay">
</div> <span>Profilbild aktualisieren</span>
</div>
<div class="form-group mb-3"> </div>
<label for="newEmail">Neue E-Mail:</label> <div class="mb-3">
<input type="email" class="form-control {% if errors.get('newEmail') %} is-invalid {% endif %}" id="newEmail" name="newEmail" value="{{email}}"> <form id="uploadForm" action="/upload" method="POST" enctype="multipart/form-data">
<div class="invalid-feedback"> <input type="file" class="form-control-file" id="profileImageInput" name="file" style="display: none;">
{{ errors.get('newEmail', '') }} </form>
</div> </div>
</div> </div>
<div class="ml-5" style="margin-left: 50px;">
<div class="form-group mb-5"> <h5 class="card-title">Name</h5>
<label for="newPassword">Neues Passwort:</label> <p>{{ name }}</p>
<input type="text" class="form-control {% if errors.get('newPassword') %} is-invalid {% endif %}" id="newPassword" name="newPassword"> <h5 class="card-title">Email</h5>
<div class="invalid-feedback"> <p>{{ email }}</p>
{{ errors.get('newPassword', '') }} <button type="button" class="btn btn-primary" id="editButton" data-toggle="modal" data-target="#editModal">
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>
</div> </div>
<!-- Password change fields -->
<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">
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="oldPassword">Altes Passwort:</label> <label for="oldPassword">Altes Passwort:</label>
<input type="password" class="form-control {% if errors.get('oldPassword') %} is-invalid {% endif %}" id="oldPassword" name="oldPassword"> <input type="password" class="form-control" id="oldPassword" name="oldPassword" autocomplete="current-password">
<div class="invalid-feedback"> <div class="invalid-feedback" id="oldPasswordFeedback"></div>
{{ errors.get('oldPassword', '') }} </div>
<div class="form-group mb-3">
<label for="newPassword">Neues Passwort:</label>
<input type="password" class="form-control" id="newPassword" name="newPassword" autocomplete="new-password">
<div class="invalid-feedback" id="newPasswordFeedback"></div>
</div>
<div class="form-group mb-3">
<label for="confirmPassword">Neues Passwort bestätigen:</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" autocomplete="new-password">
<div class="invalid-feedback" id="confirmPasswordFeedback"></div>
</div>
<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>
</div>
</div>
<button type="submit" class="btn btn-primary">Änderungen speichern</button>
</form>
<!-- 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>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog mt-20" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Bearbeiten</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">
<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">
<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">
<div class="invalid-feedback" id="emailFeedback"></div>
</div>
<div class="form-group">
<label for="password">Passwort:</label>
<input type="password" class="form-control" id="password" name="password" autocomplete="current-password">
<div class="invalid-feedback" id="passwordFeedback"></div>
</div>
</form>
</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" id="saveChangesButton">Änderungen speichern</button>
</div>
</div>
</div>
</div>
<script src="../static/script/script-profile.js"></script>
{% endblock %} {% 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 %}