Merge remote-tracking branch 'origin/master'

# Conflicts:
#	models/Habit.py
This commit is contained in:
janphilippweinsheimer 2024-02-20 10:05:07 +01:00
commit cb1991e2cc
20 changed files with 639 additions and 139 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: 95 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

123
app.py
View File

@ -1,12 +1,14 @@
import datetime import datetime
import hashlib import hashlib
import os
from PIL import Image
from flask import Flask, render_template, redirect, url_for, request 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
@ -136,6 +138,7 @@ def index():
title=name, title=name,
utc_dt=datetime.datetime.now().strftime("%d.%m.%Y %H:%M %A"), utc_dt=datetime.datetime.now().strftime("%d.%m.%Y %H:%M %A"),
habit_lists=habit_lists, habit_lists=habit_lists,
heatmap_values=current_user.get_heatmap(),
errors={}, errors={},
) )
@ -270,7 +273,7 @@ def profile():
"profile.html", "profile.html",
name=current_user.name, name=current_user.name,
email=current_user.email, email=current_user.email,
errors={} profile_image_url=current_user.profile_image,
) )
@ -279,34 +282,37 @@ def profile():
def profile_change(): def profile_change():
newName = request.form.get('newName') newName = request.form.get('newName')
newEmail = request.form.get('newEmail') 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 # Update user
current_user.name = newName current_user.name = newName
current_user.email = newEmail 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,
)
@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: if newPassword:
current_user.password = hashlib.sha256(newPassword.encode()).hexdigest() current_user.password = hashlib.sha256(newPassword.encode()).hexdigest()
current_user.update() current_user.update()
@ -316,6 +322,66 @@ def profile_change():
"profile.html", "profile.html",
name=current_user.name, name=current_user.name,
email=current_user.email, email=current_user.email,
profile_image_url=current_user.profile_image,
)
UPLOAD_FOLDER = 'static/profile_images/' # Folder to store profile images
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def save_profile_image(image_file):
filename = image_file.filename
if '.' not in filename:
# 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)
# Convert the image to RGB mode (required for JPEG)
image = image.convert('RGB')
# Determine the size of the square image
min_dimension = min(image.size)
square_size = (min_dimension, min_dimension)
# Calculate the coordinates for cropping
left = (image.width - min_dimension) / 2
top = (image.height - min_dimension) / 2
right = (image.width + min_dimension) / 2
bottom = (image.height + min_dimension) / 2
# Crop the image to a square and resize it
image = image.crop((left, top, right, bottom))
image.thumbnail(square_size)
image = image.resize((256, 256))
# Save the processed image
image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
image.save(image_path, 'JPEG', quality=100)
@app.route('/upload', methods=['POST'])
def upload_profile_image():
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
save_profile_image(file)
# Back to profile
return render_template(
"profile.html",
name=current_user.name,
email=current_user.email,
profile_image_url=current_user.profile_image,
errors={} errors={}
) )
@ -344,7 +410,7 @@ 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)
else: else:
delete_tracking.delete() delete_tracking.delete()
@ -374,7 +440,6 @@ def delete_habit():
habit.delete() habit.delete()
return {} return {}
@app.route('/reorder', methods=['POST']) @app.route('/reorder', methods=['POST'])
@login_required @login_required
def reorder_habits(): def reorder_habits():

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, 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, created_at, updated_at) VALUES "
f"'{password}', '{now}', '{now}');") f"('{name}', '{email}', '{password}', '{profile_image}', '{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,10 +66,11 @@ 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):
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}', 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}', updated_at = '{now}' WHERE id = {id};"
conn = con3() conn = con3()
@ -57,22 +82,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):
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, created_at, updated_at) "
f"'{name}', '{note}', '{times}', '{unit}', '{slot}', '{now}', '{now}');") f"VALUES ('{list_id}', '{name}', '{note}', '{times}', '{unit}', '{slot}', '{now}', '{now}');")
conn = con3() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
@ -168,7 +191,8 @@ 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() conn = con3()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query) cursor.execute(query)
@ -186,8 +210,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 +222,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 +232,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 +242,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,7 +251,7 @@ 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):
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, created_at, updated_at) "
@ -264,6 +288,37 @@ def get_habitLists(user_id: int):
return habit_lists 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 remove_user(list_id: int, user_id: int):
query = f"DELETE FROM habit_lists 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 +339,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

@ -1,9 +1,9 @@
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)
@dataclass @dataclass
@ -11,24 +11,34 @@ class HabitList:
id: int id: int
name: str name: str
description: str description: str
created_at: date habits: list = None #? unclear usage
updated_at: date
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) id = create_habitList(user_id, name, description)
return HabitList(id, name, description, datetime.now(), datetime.now()) return HabitList(id, name, description)
@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]) if habitList else None
def delete(self):
# Updates: name, description
def update(self):
update_habitList(self.id, self.name, self.description)
# Deletes the HabitList | The id of the current user is necessary
def delete(self, user_id):
if len(get_users) > 1:
self.remove_user(user_id)
else:
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:
@ -37,7 +47,9 @@ class HabitList:
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:
@ -45,3 +57,14 @@ class HabitList:
users.append(user) users.append(user)
return users return users
# Adds a User by email to the HabitList
def add_user(self, email: str):
user = User.get_by_email(email)
add_user(self.id, user.id)
# 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,75 @@
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)
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):
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
@staticmethod @staticmethod
def create(name: str, email: str, password: str): def create(name: str, email: str, password: str):
id = create_user(name, email, password) id, profile_image = create_user(name, email, password)
return User(id, name, email) return User(id=id, name=name, email=email, profile_image=profile_image)
@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]) 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]) 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)
# 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")) habitList = HabitList(habitList[0], habitList[1], habitList[2])
habitLists.append(habitList) habitLists.append(habitList)
return habitLists return habitLists
def get_heatmap(self):
# Returns all heatmap-values from the last 28 days
def get_heatmap(self) -> list:
# get current day of week as integer. monday is 0 and sunday is 6
weekday = datetime.today().weekday()
heatmap = [] heatmap = []
for day in range (0, 27):
# append the heatmap values of the current week
for day in range(0, weekday):
heatmap.append(0)
for day in range (0, 28):
value = get_heatmap_value(self.id, day) value = get_heatmap_value(self.id, day)
heatmap.append(value) heatmap.append(value)
return heatmap return heatmap

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -6,12 +6,24 @@
<title>{{ title }} - HabitTracker</title> <title>{{ title }} - HabitTracker</title>
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="/static/main.css"> <link rel="stylesheet" type="text/css" href="../../static/css/background.css">
<link rel="stylesheet" type="text/css" href="../../static/css/profile.css">
<link href="https://cdn.jsdelivr.net/npm/fastbootstrap@2.2.0/dist/css/fastbootstrap.min.css" rel="stylesheet" integrity="sha256-V6lu+OdYNKTKTsVFBuQsyIlDiRWiOmtC8VQ8Lzdm2i4=" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/fastbootstrap@2.2.0/dist/css/fastbootstrap.min.css" rel="stylesheet" integrity="sha256-V6lu+OdYNKTKTsVFBuQsyIlDiRWiOmtC8VQ8Lzdm2i4=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- 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 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>
<!-- 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">
@ -51,9 +63,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,289 @@
{% 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 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>
</div> </div>
</div> </div>
<!-- Password change fields -->
<div class="card 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>
</form>
<button type="button" class="btn btn-primary" id="submitPasswordChange">Änderungen speichern</button>
</div> </div>
</div> </div>
</div>
<button type="submit" class="btn btn-primary">Änderungen speichern</button>
</form>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-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>
document.addEventListener("DOMContentLoaded", function() {
// Get elements
const profileImage = document.getElementById("profileImage");
const profileImageOverlay = document.getElementById("profileImageOverlay");
const profileImageInput = document.getElementById("profileImageInput");
const uploadForm = document.getElementById("uploadForm");
const editButton = document.getElementById("editButton");
const saveChangesButton = document.getElementById("saveChangesButton");
const editForm = document.getElementById("editForm");
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
const submitPasswordChangeButton = document.getElementById("submitPasswordChange");
// Open file input when profile image is clicked
profileImageOverlay.addEventListener("click", function() {
profileImageInput.click();
});
// Change profile image when a new file is selected
profileImageInput.addEventListener("change", function() {
const file = this.files[0];
const reader = new FileReader();
reader.onload = function(e) {
profileImage.src = e.target.result;
};
reader.readAsDataURL(file);
// Submit the form
uploadForm.submit();
});
// Add event listener to edit button to open modal
editButton.addEventListener("click", function() {
editModal.show();
document.getElementById("newName").value = "{{ name }}";
document.getElementById("newEmail").value = "{{ email }}";
document.getElementById("password").value = "";
});
// Add event listener to save changes button to submit form
saveChangesButton.addEventListener("click", function() {
// Perform client-side validation before submitting the form
validateForm()
.then(isValid => {
if (isValid) {
console.log("Validated 2");
editForm.submit();
}
})
.catch(error => {
// Handle validation error
console.log("Account Form validation failed", error);
});
});
// Function to perform client-side validation
async function validateForm() {
let isValid = true;
isValid = validateInput("newName", "nameFeedback", "Bitte geben Sie einen neuen Namen ein.") && isValid;
isValid = validateEmail("newEmail", "emailFeedback", "Bitte geben Sie eine gültige E-Mail-Adresse ein.") && isValid;
try {
const passwordValid = await validatePassword("password", "passwordFeedback", "Bitte geben Sie Ihr Passwort ein.");
isValid = passwordValid && isValid;
} catch (error) {
isValid = false;
}
return isValid;
}
// Function to validate input fields
function validateInput(inputId, feedbackId, errorMessage) {
const input = document.getElementById(inputId);
const feedback = document.getElementById(feedbackId);
if (!input.value.trim()) {
feedback.textContent = errorMessage;
input.classList.add("is-invalid");
return false;
} else {
feedback.textContent = "";
input.classList.remove("is-invalid");
return true;
}
}
// Function to validate email
function validateEmail(emailId, feedbackId, errorMessage) {
const input = document.getElementById(emailId);
const feedback = document.getElementById(feedbackId);
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Regular expression for email validation
if (!input.value.trim()) {
feedback.textContent = "Bitte geben Sie eine neue E-Mail-Adresse ein.";
input.classList.add("is-invalid");
return false;
} else if (!emailRegex.test(input.value.trim())) {
feedback.textContent = errorMessage;
input.classList.add("is-invalid");
return false;
} else {
feedback.textContent = "";
input.classList.remove("is-invalid");
return true;
}
}
// Function to validate password
function validatePassword(passwordId, passwordFeedbackId, errorMessage) {
return new Promise((resolve, reject) => {
const passwordInput = document.getElementById(passwordId);
const passwordFeedback = document.getElementById(passwordFeedbackId);
const password = passwordInput.value.trim(); // Get the password entered by the user
if (!passwordInput.value.trim()) {
passwordFeedback.textContent = errorMessage;
passwordInput.classList.add("is-invalid");
reject(false);
} else {
axios.post('/check_password', { password: password })
.then(response => {
if (!response.data.valid) {
passwordFeedback.textContent = "Falsches Passwort.";
passwordInput.classList.add("is-invalid");
passwordInput.value = "";
reject(false);
} else {
passwordInput.classList.remove("is-invalid");
resolve(true);
}
})
.catch(error => {
console.error('Error checking password:', error);
reject(false);
});
}
});
}
submitPasswordChangeButton.addEventListener("click", function() {
// Perform client-side validation before submitting the form
validatePasswordChangeForm()
.then(isValid => {
if (isValid) {
document.getElementById("editPasswordForm").submit();
}
})
.catch(error => {
// Handle validation error
console.log("Password change form validation failed", error);
});
});
async function validatePasswordChangeForm() {
let isValid = true;
try {
const passwordValid = await validatePassword("oldPassword", "oldPasswordFeedback", "Bitte geben Sie Ihr altes Passwort ein.");
isValid = passwordValid && isValid;
} catch (error) {
isValid = false;
}
isValid = validateInput("newPassword", "newPasswordFeedback", "Bitte geben Sie Ihr neues Passwort ein.") && isValid;
isValid = validateInput("confirmPassword", "confirmPasswordFeedback", "Bitte bestätigen Sie Ihr neues Passwort.") && isValid;
// Check if new password matches confirm password
const newPassword = document.getElementById("newPassword").value.trim();
const confirmPassword = document.getElementById("confirmPassword").value.trim();
if (newPassword !== confirmPassword) {
document.getElementById("confirmPasswordFeedback").textContent = "Die Passwörter stimmen nicht überein.";
document.getElementById("confirmPassword").classList.add("is-invalid");
isValid = false;
} else {
document.getElementById("confirmPasswordFeedback").textContent = "";
document.getElementById("confirmPassword").classList.remove("is-invalid");
}
return isValid;
}
});
</script>
{% endblock %} {% endblock %}