Dashboard V2
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,15 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from flask import Flask, render_template, request, redirect, url_for, session, g, flash
|
from flask import Flask, render_template, request, redirect, url_for, session, g, flash, jsonify
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
import smtplib, ssl, secrets
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import json
|
||||||
|
|
||||||
|
SMTP_SERVER = "smtp.gmail.com"
|
||||||
|
SMTP_PORT = 465
|
||||||
|
EMAIL_SENDER = "clickcandit@gmail.com"
|
||||||
|
EMAIL_PASSWORD = "iuxexntistlwilhl"
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = "SUPER_SECRET_KEY" # Bitte anpassen
|
app.secret_key = "SUPER_SECRET_KEY" # Bitte anpassen
|
||||||
@@ -55,6 +63,14 @@ def init_db():
|
|||||||
is_read INTEGER DEFAULT 0
|
is_read INTEGER DEFAULT 0
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS reset_tokens (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
# Time Tracking
|
# Time Tracking
|
||||||
db.execute("""
|
db.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS time_entries (
|
CREATE TABLE IF NOT EXISTS time_entries (
|
||||||
@@ -66,15 +82,41 @@ def init_db():
|
|||||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
# Settings (OPTIONAL, falls du globale User-Einstellungen abspeichern willst)
|
||||||
|
db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
wallpaper TEXT DEFAULT '1.png',
|
||||||
|
city TEXT DEFAULT '',
|
||||||
|
show_forecast INTEGER DEFAULT 0,
|
||||||
|
bookmarks TEXT DEFAULT '',
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
wallpaper TEXT DEFAULT '19.png',
|
||||||
|
city TEXT DEFAULT 'Berlin',
|
||||||
|
show_forecast INTEGER DEFAULT 1,
|
||||||
|
bookmarks TEXT DEFAULT '[]',
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# HILFSFUNKTIONEN
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
def get_user_by_username_or_email(user_input):
|
def get_user_by_username_or_email(user_input):
|
||||||
db = get_db()
|
db = get_db()
|
||||||
user = db.execute("""
|
return db.execute("""
|
||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE username = :val OR email = :val
|
WHERE username = :val OR email = :val
|
||||||
""", {"val": user_input}).fetchone()
|
""", {"val": user_input}).fetchone()
|
||||||
return user
|
|
||||||
|
|
||||||
def get_user_by_username(username):
|
def get_user_by_username(username):
|
||||||
db = get_db()
|
db = get_db()
|
||||||
@@ -87,13 +129,18 @@ def get_user_by_id(user_id):
|
|||||||
def is_admin():
|
def is_admin():
|
||||||
return session.get('is_admin', 0) == 1
|
return session.get('is_admin', 0) == 1
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# STARTSEITE
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
if 'user_id' in session:
|
if 'user_id' in session:
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
# --------------------- REGISTRIEREN -------------------------
|
# ------------------------------------------------------------
|
||||||
|
# REGISTRIEREN: erster User wird Admin
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@@ -114,7 +161,7 @@ def register():
|
|||||||
|
|
||||||
# Erster registrierter User wird Admin
|
# Erster registrierter User wird Admin
|
||||||
count = db.execute("SELECT COUNT(*) as cnt FROM users").fetchone()['cnt']
|
count = db.execute("SELECT COUNT(*) as cnt FROM users").fetchone()['cnt']
|
||||||
is_admin_val = 1 if count == 0 else 0 # Falls noch niemand existiert, ist das der Admin
|
is_admin_val = 1 if count == 0 else 0
|
||||||
|
|
||||||
hashed_pw = generate_password_hash(password)
|
hashed_pw = generate_password_hash(password)
|
||||||
db.execute("""
|
db.execute("""
|
||||||
@@ -122,11 +169,25 @@ def register():
|
|||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
""", (username, hashed_pw, email, is_admin_val))
|
""", (username, hashed_pw, email, is_admin_val))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# OPTIONAL: initialer Eintrag in user_settings
|
||||||
|
if is_admin_val == 1:
|
||||||
|
# Falls du globale Settings pro Benutzer willst
|
||||||
|
user_id = db.execute("SELECT id FROM users WHERE username=?",(username,)).fetchone()['id']
|
||||||
|
db.execute("""
|
||||||
|
INSERT INTO user_settings (user_id, wallpaper, city, show_forecast, bookmarks)
|
||||||
|
VALUES (?, '1.png', '', 0, '')
|
||||||
|
""", (user_id,))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
flash('Registrierung erfolgreich! Bitte melde dich an.', 'green')
|
flash('Registrierung erfolgreich! Bitte melde dich an.', 'green')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
return render_template('register.html')
|
return render_template('register.html')
|
||||||
|
|
||||||
# --------------------- LOGIN/LOGOUT -------------------------
|
# ------------------------------------------------------------
|
||||||
|
# LOGIN / LOGOUT
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@@ -149,33 +210,107 @@ def logout():
|
|||||||
flash("Erfolgreich abgemeldet!", "green")
|
flash("Erfolgreich abgemeldet!", "green")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
# --------------------- PASSWORT VERGESSEN -------------------
|
# ------------------------------------------------------------
|
||||||
|
# PASSWORT VERGESSEN
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
|
||||||
|
def reset_password(token):
|
||||||
|
db = get_db()
|
||||||
|
row = db.execute("""
|
||||||
|
SELECT * FROM reset_tokens
|
||||||
|
WHERE token = ?
|
||||||
|
""", (token,)).fetchone()
|
||||||
|
if not row:
|
||||||
|
flash("Ungültiges oder bereits benutztes Token!", "red")
|
||||||
|
return redirect(url_for('forgot_password'))
|
||||||
|
|
||||||
|
# Ablaufdatum prüfen
|
||||||
|
expires_at = datetime.fromisoformat(row['expires_at'])
|
||||||
|
if datetime.now() > expires_at:
|
||||||
|
flash("Token abgelaufen! Bitte erneut anfordern.", "red")
|
||||||
|
db.execute("DELETE FROM reset_tokens WHERE token=?", (token,))
|
||||||
|
db.commit()
|
||||||
|
return redirect(url_for('forgot_password'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
pw1 = request.form.get('pw1')
|
||||||
|
pw2 = request.form.get('pw2')
|
||||||
|
if not pw1 or not pw2:
|
||||||
|
flash("Bitte beide Felder ausfüllen!", "red")
|
||||||
|
return redirect(url_for('reset_password', token=token))
|
||||||
|
|
||||||
|
if pw1 != pw2:
|
||||||
|
flash("Passwörter stimmen nicht überein!", "red")
|
||||||
|
return redirect(url_for('reset_password', token=token))
|
||||||
|
|
||||||
|
hashed_pw = generate_password_hash(pw1)
|
||||||
|
# Passwort setzen
|
||||||
|
db.execute("UPDATE users SET password=? WHERE id=?", (hashed_pw, row['user_id']))
|
||||||
|
# Token direkt löschen
|
||||||
|
db.execute("DELETE FROM reset_tokens WHERE token=?", (token,))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
flash("Passwort erfolgreich zurückgesetzt! Du kannst dich jetzt einloggen.", "green")
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
# GET: Formular
|
||||||
|
return render_template('reset_password.html', token=token)
|
||||||
|
|
||||||
@app.route('/forgot_password', methods=['GET', 'POST'])
|
@app.route('/forgot_password', methods=['GET', 'POST'])
|
||||||
def forgot_password():
|
def forgot_password():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
user_input = request.form.get('username_or_email')
|
user_input = request.form.get('username_or_email')
|
||||||
if not user_input:
|
if not user_input:
|
||||||
flash("Bitte einen Benutzernamen oder eine E-Mail angeben!", "red")
|
flash("Bitte Benutzernamen oder E-Mail eingeben!", "red")
|
||||||
return redirect(url_for('forgot_password'))
|
return redirect(url_for('forgot_password'))
|
||||||
|
|
||||||
user = get_user_by_username_or_email(user_input)
|
db = get_db()
|
||||||
# Hier könnte man z.B. eine E-Mail mit Link senden oder im Notfall ein neues PW setzen
|
# Schaue nach Username oder Email
|
||||||
|
user = db.execute("""
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE username=? OR email=?
|
||||||
|
""", (user_input, user_input)).fetchone()
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
# Demo: Wir setzen einfach das Passwort auf "reset123" (nicht sicher!)
|
# Token generieren
|
||||||
# Besser: generiere Token, sende E-Mail etc.
|
token = secrets.token_urlsafe(32)
|
||||||
new_pw_hashed = generate_password_hash("reset123")
|
expires = datetime.now() + timedelta(hours=1)
|
||||||
db = get_db()
|
|
||||||
db.execute("UPDATE users SET password=? WHERE id=?", (new_pw_hashed, user['id']))
|
db.execute("""
|
||||||
|
INSERT INTO reset_tokens (user_id, token, expires_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (user['id'], token, expires))
|
||||||
db.commit()
|
db.commit()
|
||||||
flash("Dein Passwort wurde zurückgesetzt auf 'reset123'. Bitte ändere es nach dem Login!", "green")
|
|
||||||
|
reset_link = f"{request.url_root}reset_password/{token}"
|
||||||
|
subject = "Passwort zurücksetzen"
|
||||||
|
body = f"""Hallo {user['username']},
|
||||||
|
|
||||||
|
klicke auf folgenden Link, um dein Passwort zurückzusetzen (gültig 1 Stunde):
|
||||||
|
{reset_link}
|
||||||
|
|
||||||
|
Wenn du das nicht angefordert hast, ignoriere diese Nachricht.
|
||||||
|
"""
|
||||||
|
message = f"Subject: {subject}\n\n{body}"
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context) as server:
|
||||||
|
server.login(EMAIL_SENDER, EMAIL_PASSWORD)
|
||||||
|
server.sendmail(EMAIL_SENDER, user['email'], message)
|
||||||
|
flash("Eine E-Mail zum Zurücksetzen deines Passworts wurde versendet!", "green")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Fehler beim Senden der E-Mail: {e}", "red")
|
||||||
|
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
else:
|
else:
|
||||||
flash("Kein passender Benutzer gefunden. Versuche es erneut!", "red")
|
flash("Kein Nutzer gefunden!", "red")
|
||||||
return redirect(url_for('forgot_password'))
|
return redirect(url_for('forgot_password'))
|
||||||
|
|
||||||
return render_template('forgot_password.html')
|
return render_template('forgot_password.html')
|
||||||
|
# ------------------------------------------------------------
|
||||||
# --------------------- ADMIN-BEREICH ------------------------
|
# ADMIN-BEREICH
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/admin', methods=['GET', 'POST'])
|
@app.route('/admin', methods=['GET', 'POST'])
|
||||||
def admin_panel():
|
def admin_panel():
|
||||||
if not is_admin():
|
if not is_admin():
|
||||||
@@ -214,7 +349,9 @@ def delete_user(user_id):
|
|||||||
flash("Benutzer gelöscht!", "green")
|
flash("Benutzer gelöscht!", "green")
|
||||||
return redirect(url_for('admin_panel'))
|
return redirect(url_for('admin_panel'))
|
||||||
|
|
||||||
# --------------------- NOTIFICATIONS ------------------------
|
# ------------------------------------------------------------
|
||||||
|
# BENACHRICHTIGUNGEN
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/admin/notifications', methods=['POST'])
|
@app.route('/admin/notifications', methods=['POST'])
|
||||||
def add_notification():
|
def add_notification():
|
||||||
if not is_admin():
|
if not is_admin():
|
||||||
@@ -233,7 +370,9 @@ def add_notification():
|
|||||||
flash("Benachrichtigung erstellt!", "green")
|
flash("Benachrichtigung erstellt!", "green")
|
||||||
return redirect(url_for('admin_panel'))
|
return redirect(url_for('admin_panel'))
|
||||||
|
|
||||||
# --------------------- BOOKMARKS (nur Admin pflegbar) -------
|
# ------------------------------------------------------------
|
||||||
|
# BOOKMARKS
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/admin/bookmarks/<int:user_id>', methods=['GET', 'POST'])
|
@app.route('/admin/bookmarks/<int:user_id>', methods=['GET', 'POST'])
|
||||||
def manage_bookmarks(user_id):
|
def manage_bookmarks(user_id):
|
||||||
if not is_admin():
|
if not is_admin():
|
||||||
@@ -244,13 +383,14 @@ def manage_bookmarks(user_id):
|
|||||||
if not user:
|
if not user:
|
||||||
flash("Benutzer nicht gefunden!", "red")
|
flash("Benutzer nicht gefunden!", "red")
|
||||||
return redirect(url_for('admin_panel'))
|
return redirect(url_for('admin_panel'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
title = request.form.get('title')
|
title = request.form.get('title')
|
||||||
url = request.form.get('url')
|
url_ = request.form.get('url')
|
||||||
icon = request.form.get('icon_class', 'fas fa-bookmark')
|
icon = request.form.get('icon_class', 'fas fa-bookmark')
|
||||||
if title and url:
|
if title and url_:
|
||||||
db.execute("INSERT INTO bookmarks (user_id, title, url, icon_class) VALUES (?, ?, ?, ?)",
|
db.execute("INSERT INTO bookmarks (user_id, title, url, icon_class) VALUES (?, ?, ?, ?)",
|
||||||
(user_id, title, url, icon))
|
(user_id, title, url_, icon))
|
||||||
db.commit()
|
db.commit()
|
||||||
flash("Neues Lesezeichen hinzugefügt!", "green")
|
flash("Neues Lesezeichen hinzugefügt!", "green")
|
||||||
|
|
||||||
@@ -268,7 +408,9 @@ def delete_bookmark(bookmark_id, user_id):
|
|||||||
flash("Lesezeichen gelöscht!", "green")
|
flash("Lesezeichen gelöscht!", "green")
|
||||||
return redirect(url_for('manage_bookmarks', user_id=user_id))
|
return redirect(url_for('manage_bookmarks', user_id=user_id))
|
||||||
|
|
||||||
# --------------------- ZEITERFASSUNG ------------------------
|
# ------------------------------------------------------------
|
||||||
|
# ZEITERFASSUNG
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/time_tracking', methods=['POST'])
|
@app.route('/time_tracking', methods=['POST'])
|
||||||
def time_tracking():
|
def time_tracking():
|
||||||
if 'user_id' not in session:
|
if 'user_id' not in session:
|
||||||
@@ -306,43 +448,193 @@ def time_tracking():
|
|||||||
|
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
# --------------------- DASHBOARD ----------------------------
|
# ------------------------------------------------------------
|
||||||
|
# DASHBOARD
|
||||||
|
# ------------------------------------------------------------
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
def dashboard():
|
def dashboard():
|
||||||
if 'user_id' not in session:
|
if 'user_id' not in session:
|
||||||
flash("Bitte erst einloggen!", "red")
|
flash("Bitte melde dich an!", "red")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
db = get_db()
|
|
||||||
user_id = session['user_id']
|
user_id = session['user_id']
|
||||||
user = get_user_by_id(user_id)
|
db = get_db()
|
||||||
|
|
||||||
# Notifications (global oder speziell für diesen User)
|
# User abfragen
|
||||||
notifications = db.execute("""
|
user = db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
||||||
SELECT * FROM notifications
|
if not user:
|
||||||
WHERE user_id = ? OR user_id IS NULL
|
flash("Benutzer nicht gefunden.", "red")
|
||||||
ORDER BY created_at DESC
|
return redirect(url_for('logout'))
|
||||||
""", (user_id,)).fetchall()
|
|
||||||
|
|
||||||
# Bookmarks für diesen Benutzer
|
# Settings aus user_settings
|
||||||
user_bookmarks = db.execute("SELECT * FROM bookmarks WHERE user_id = ?", (user_id,)).fetchall()
|
settings = db.execute("SELECT * FROM user_settings WHERE user_id = ?", (user_id,)).fetchone()
|
||||||
|
|
||||||
# Time-Entries
|
# Standardwerte
|
||||||
time_entries = db.execute("""
|
if not settings:
|
||||||
SELECT * FROM time_entries
|
wallpaper = '19.png'
|
||||||
WHERE user_id = ?
|
city = 'Berlin'
|
||||||
ORDER BY start_time DESC
|
show_forecast = True
|
||||||
""", (user_id,)).fetchall()
|
bookmarks = []
|
||||||
|
else:
|
||||||
|
wallpaper = settings['wallpaper']
|
||||||
|
city = settings['city']
|
||||||
|
show_forecast = bool(settings['show_forecast'])
|
||||||
|
if settings['bookmarks']:
|
||||||
|
bookmarks = settings['bookmarks'].split(",")
|
||||||
|
else:
|
||||||
|
bookmarks = []
|
||||||
|
|
||||||
return render_template('dashboard.html',
|
# Wetter holen (wenn gewünscht)
|
||||||
user=user['username'],
|
current_temp, weather_icon, forecast = get_weather(city)
|
||||||
notifications=notifications,
|
if current_temp is None:
|
||||||
user_bookmarks=user_bookmarks,
|
current_temp = "N/A"
|
||||||
time_entries=time_entries,
|
weather_icon = "fa-question"
|
||||||
domain="meinedomain.de",
|
forecast = []
|
||||||
logo_path="static/logo.png")
|
|
||||||
|
|
||||||
# --------------------- STARTUP ------------------------------
|
# Domain, Logo usw.
|
||||||
|
domain = "example.com"
|
||||||
|
logo_path = url_for('static', filename='clickcandit.png')
|
||||||
|
|
||||||
|
# Beispiel-Apps
|
||||||
|
user_app_chunks = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'name': 'Mail',
|
||||||
|
'subdomain': 'mail',
|
||||||
|
'appkey': 'mailapp',
|
||||||
|
'icon_class': 'fa fa-envelope',
|
||||||
|
'bg_color': 'bg-blue-500'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Calendar',
|
||||||
|
'subdomain': 'calendar',
|
||||||
|
'appkey': 'calendarapp',
|
||||||
|
'icon_class': 'fa fa-calendar',
|
||||||
|
'bg_color': 'bg-green-500'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'dashboard.html',
|
||||||
|
user=user['username'],
|
||||||
|
wallpaper=wallpaper,
|
||||||
|
city=city,
|
||||||
|
show_forecast=show_forecast,
|
||||||
|
bookmarks=bookmarks,
|
||||||
|
current_temp=current_temp,
|
||||||
|
weather_icon=weather_icon,
|
||||||
|
forecast=forecast,
|
||||||
|
domain=domain,
|
||||||
|
logo_path=logo_path,
|
||||||
|
user_app_chunks=user_app_chunks
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# SUPPORT & SETTINGS ROUTEN (Aus V1)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
@app.route('/send_support_message', methods=['POST'])
|
||||||
|
def send_support_message():
|
||||||
|
"""
|
||||||
|
Beispiel-Endpunkt, den dein Support-Modal per Fetch aufruft.
|
||||||
|
Erwartet JSON-Daten: { "email": ..., "problemType": ..., "message": ... }
|
||||||
|
"""
|
||||||
|
data = request.get_json()
|
||||||
|
email = data.get('email')
|
||||||
|
problem_type = data.get('problemType')
|
||||||
|
message = data.get('message')
|
||||||
|
|
||||||
|
if not email or not message:
|
||||||
|
return jsonify({"success": False, "error": "Fehlende Felder"}), 400
|
||||||
|
|
||||||
|
# Hier könntest du z.B. eine E-Mail verschicken oder einen Eintrag in der DB anlegen
|
||||||
|
# Demo: wir legen einfach einen Notification-Eintrag an
|
||||||
|
db = get_db()
|
||||||
|
db.execute("""
|
||||||
|
INSERT INTO notifications (user_id, message)
|
||||||
|
VALUES (NULL, ?)
|
||||||
|
""", (f"Support-Anfrage ({problem_type}) von {email}: {message}",))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
@app.route('/get_settings', methods=['GET'])
|
||||||
|
def get_settings():
|
||||||
|
"""
|
||||||
|
Liefert das aktuell gespeicherte Setting zurück (V1-Modal).
|
||||||
|
Du brauchst user_settings oder ein eigenes Modell, wo du die Bookmarks/Stadt/etc. speicherst
|
||||||
|
"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({"error": "not logged in"}), 403
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
settings_row = db.execute("SELECT * FROM user_settings WHERE user_id=?", (session['user_id'],)).fetchone()
|
||||||
|
if settings_row:
|
||||||
|
return jsonify({
|
||||||
|
"wallpaper": settings_row['wallpaper'],
|
||||||
|
"city": settings_row['city'],
|
||||||
|
"show_forecast": bool(settings_row['show_forecast']),
|
||||||
|
"bookmarks": settings_row['bookmarks'].split(",") if settings_row['bookmarks'] else []
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Falls noch kein Datensatz existiert
|
||||||
|
return jsonify({
|
||||||
|
"wallpaper": "1.png",
|
||||||
|
"city": "",
|
||||||
|
"show_forecast": False,
|
||||||
|
"bookmarks": []
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/save_settings', methods=['POST'])
|
||||||
|
def save_settings():
|
||||||
|
"""
|
||||||
|
Speichert die Einstellungen, die vom Settings-Modal per Fetch geschickt werden
|
||||||
|
JSON body: { "wallpaper": ..., "city": ..., "show_forecast": ..., "bookmarks": ... }
|
||||||
|
"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({"success": False, "error": "not logged in"}), 403
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
wallpaper = data.get('wallpaper', '1.png')
|
||||||
|
city = data.get('city', '')
|
||||||
|
show_forecast = data.get('show_forecast', False)
|
||||||
|
bookmarks_list = data.get('bookmarks', [])
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
# Check if row exists
|
||||||
|
existing = db.execute("SELECT id FROM user_settings WHERE user_id=?", (session['user_id'],)).fetchone()
|
||||||
|
if existing:
|
||||||
|
db.execute("""
|
||||||
|
UPDATE user_settings
|
||||||
|
SET wallpaper=?, city=?, show_forecast=?, bookmarks=?
|
||||||
|
WHERE user_id=?
|
||||||
|
""", (wallpaper, city, int(show_forecast), ",".join(bookmarks_list), session['user_id']))
|
||||||
|
else:
|
||||||
|
db.execute("""
|
||||||
|
INSERT INTO user_settings (user_id, wallpaper, city, show_forecast, bookmarks)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""", (session['user_id'], wallpaper, city, int(show_forecast), ",".join(bookmarks_list)))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
def get_weather(city):
|
||||||
|
"""
|
||||||
|
Gibt (current_temp, weather_icon, forecast) zurück
|
||||||
|
"""
|
||||||
|
# Hier kannst du z. B. eine Wetter-API anfragen
|
||||||
|
# oder Dummy-Werte für den Anfang setzen
|
||||||
|
|
||||||
|
# DUMMY Beispiel:
|
||||||
|
current_temp = 24
|
||||||
|
weather_icon = "fa-cloud"
|
||||||
|
forecast = []
|
||||||
|
return (current_temp, weather_icon, forecast)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# STARTUP
|
||||||
|
# ------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
init_db()
|
init_db()
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|||||||
Binary file not shown.
@@ -1,12 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!-- templates/admin.html -->
|
||||||
<html lang="de" class="dark">
|
{% extends "base.html" %}
|
||||||
<head>
|
{% block title %}Admin-Bereich{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Admin-Bereich</title>
|
{% block content %}
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<div class="container mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
</head>
|
<h1 class="text-3xl font-bold mb-6">Admin-Bereich</h1>
|
||||||
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-white p-6">
|
|
||||||
<h1 class="text-3xl font-bold mb-4">Admin-Bereich</h1>
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -18,122 +16,134 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<!-- Neuen Benutzer anlegen -->
|
<!-- Benutzer anlegen -->
|
||||||
<div class="mb-8 p-4 rounded bg-white dark:bg-gray-800">
|
<div class="glassmorphism p-4 rounded shadow-lg mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-2">Neuen Benutzer anlegen</h2>
|
<h2 class="text-xl font-semibold mb-2">Neuen Benutzer anlegen</h2>
|
||||||
<form method="POST">
|
<form method="POST" action="{{ url_for('admin_panel') }}" class="space-y-4">
|
||||||
<div class="mb-2">
|
<div>
|
||||||
<label class="block mb-1" for="new_username">Benutzername</label>
|
<label class="block mb-1">Benutzername</label>
|
||||||
<input type="text" name="new_username" id="new_username" class="p-2 border rounded w-full" required>
|
<input type="text" name="new_username" class="p-2 border rounded w-full" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div>
|
||||||
<label class="block mb-1" for="new_password">Passwort</label>
|
<label class="block mb-1">Passwort</label>
|
||||||
<input type="password" name="new_password" id="new_password" class="p-2 border rounded w-full" required>
|
<input type="password" name="new_password" class="p-2 border rounded w-full" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label class="inline-flex items-center">
|
<label class="inline-flex items-center">
|
||||||
<input type="checkbox" name="new_is_admin" class="form-checkbox h-5 w-5 text-blue-600">
|
<input type="checkbox" name="new_is_admin" class="form-checkbox h-5 w-5 text-blue-600" />
|
||||||
<span class="ml-2">Als Admin markieren</span>
|
<span class="ml-2">Als Admin markieren</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">Anlegen</button>
|
<button type="submit" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
|
||||||
</form>
|
Erstellen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Benutzer-Übersicht oder einzelner Benutzer für Bookmarks -->
|
<!-- Benutzerübersicht -->
|
||||||
{% if users is not none %}
|
<div class="glassmorphism p-4 rounded shadow-lg mb-8">
|
||||||
<div class="mb-8 p-4 rounded bg-white dark:bg-gray-800">
|
<h2 class="text-xl font-semibold mb-2">Benutzerverwaltung</h2>
|
||||||
<h2 class="text-xl font-semibold mb-2">Benutzerverwaltung</h2>
|
<table class="w-full table-auto">
|
||||||
<table class="table-auto w-full">
|
<thead>
|
||||||
<thead>
|
<tr class="border-b">
|
||||||
<tr>
|
<th class="px-2 py-1">ID</th>
|
||||||
<th class="px-2 py-1 border-b">ID</th>
|
<th class="px-2 py-1">Username</th>
|
||||||
<th class="px-2 py-1 border-b">Benutzername</th>
|
<th class="px-2 py-1">Admin?</th>
|
||||||
<th class="px-2 py-1 border-b">Admin?</th>
|
<th class="px-2 py-1"></th>
|
||||||
<th class="px-2 py-1 border-b">Aktion</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{% for u in users %}
|
||||||
{% for user in users %}
|
<tr class="border-b">
|
||||||
<tr>
|
<td class="px-2 py-1">{{ u.id }}</td>
|
||||||
<td class="px-2 py-1 border-b">{{ user.id }}</td>
|
<td class="px-2 py-1">{{ u.username }}</td>
|
||||||
<td class="px-2 py-1 border-b">{{ user.username }}</td>
|
<td class="px-2 py-1">{{ 'Ja' if u.is_admin else 'Nein' }}</td>
|
||||||
<td class="px-2 py-1 border-b">{{ 'Ja' if user.is_admin else 'Nein' }}</td>
|
<td class="px-2 py-1 text-right">
|
||||||
<td class="px-2 py-1 border-b">
|
{% if u.id != session.user_id %}
|
||||||
<form action="{{ url_for('delete_user', user_id=user.id) }}" method="POST" class="inline-block" onsubmit="return confirm('Benutzer wirklich löschen?')">
|
<form method="POST" action="{{ url_for('delete_user', user_id=u.id) }}" onsubmit="return confirm('Benutzer wirklich löschen?')">
|
||||||
<button class="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600 text-sm">Löschen</button>
|
<button class="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600 text-sm">
|
||||||
</form>
|
Löschen
|
||||||
<a href="{{ url_for('manage_bookmarks', user_id=user.id) }}" class="bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600 text-sm ml-2">Lesezeichen</a>
|
</button>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if single_user is defined and single_user %}
|
|
||||||
<!-- Bookmarks verwalten für einen User -->
|
|
||||||
<div class="mb-8 p-4 rounded bg-white dark:bg-gray-800">
|
|
||||||
<h2 class="text-xl font-semibold mb-2">Lesezeichen für {{ single_user.username }}</h2>
|
|
||||||
<form method="POST">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="block mb-1" for="title">Titel</label>
|
|
||||||
<input type="text" name="title" id="title" class="p-2 border rounded w-full" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="block mb-1" for="url">URL</label>
|
|
||||||
<input type="text" name="url" id="url" class="p-2 border rounded w-full" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="block mb-1" for="icon_class">Icon (FontAwesome Klasse)</label>
|
|
||||||
<input type="text" name="icon_class" id="icon_class" class="p-2 border rounded w-full" placeholder="z.B. fas fa-bookmark">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">Hinzufügen</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Bookmark-Liste -->
|
|
||||||
<ul class="mt-4 space-y-2">
|
|
||||||
{% for bm in bookmarks %}
|
|
||||||
<li class="flex items-center justify-between bg-gray-100 dark:bg-gray-700 p-2 rounded">
|
|
||||||
<div>
|
|
||||||
<i class="{{ bm.icon_class }} mr-2"></i>
|
|
||||||
<a href="{{ bm.url }}" target="_blank" class="text-blue-600 hover:underline">{{ bm.title }}</a>
|
|
||||||
</div>
|
|
||||||
<form action="{{ url_for('delete_bookmark', bookmark_id=bm.id, user_id=single_user.id) }}" method="POST" onsubmit="return confirm('Lesezeichen wirklich löschen?')">
|
|
||||||
<button class="text-red-500 hover:text-red-700"><i class="fas fa-trash-alt"></i></button>
|
|
||||||
</form>
|
</form>
|
||||||
</li>
|
{% else %}
|
||||||
|
<span class="text-xs text-gray-400">[Eigener Account]</span>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ url_for('manage_bookmarks', user_id=u.id) }}" class="ml-3 bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600 text-sm">
|
||||||
|
Lesezeichen
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Benachrichtigungen für einzelne User oder für alle -->
|
|
||||||
<div class="mb-8 p-4 rounded bg-white dark:bg-gray-800">
|
|
||||||
<h2 class="text-xl font-semibold mb-2">Benachrichtigung erstellen</h2>
|
|
||||||
<form action="{{ url_for('add_notification') }}" method="POST">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="block mb-1" for="message">Nachricht</label>
|
|
||||||
<textarea name="message" id="message" rows="2" class="p-2 border rounded w-full" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="block mb-1" for="user_id">Für Benutzer</label>
|
|
||||||
<select name="user_id" id="user_id" class="p-2 border rounded w-full">
|
|
||||||
<option value="all">Alle</option>
|
|
||||||
{% if users %}
|
|
||||||
{% for u in users %}
|
|
||||||
<option value="{{ u.id }}">{{ u.username }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="bg-purple-500 text-white py-2 px-4 rounded hover:bg-purple-600">Senden</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Benachrichtigung erstellen -->
|
||||||
|
<div class="glassmorphism p-4 rounded shadow-lg mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">Benachrichtigung erstellen</h2>
|
||||||
|
<form method="POST" action="{{ url_for('add_notification') }}" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block mb-1">Nachricht</label>
|
||||||
|
<textarea name="message" class="p-2 border rounded w-full" rows="2" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block mb-1">Für Benutzer</label>
|
||||||
|
<select name="user_id" class="p-2 border rounded w-full">
|
||||||
|
<option value="all">Alle</option>
|
||||||
|
{% for u in users %}
|
||||||
|
<option value="{{ u.id }}">{{ u.username }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="bg-purple-500 text-white py-2 px-4 rounded hover:bg-purple-600">
|
||||||
|
Senden
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Falls man Lesezeichen für EINEN User verwaltet -->
|
||||||
|
{% if single_user is defined and single_user %}
|
||||||
|
<div class="glassmorphism p-4 rounded shadow-lg mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-2">
|
||||||
|
Lesezeichen für {{ single_user.username }}
|
||||||
|
</h2>
|
||||||
|
<form method="POST" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block mb-1">Titel</label>
|
||||||
|
<input type="text" name="title" class="p-2 border rounded w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block mb-1">URL</label>
|
||||||
|
<input type="text" name="url" class="p-2 border rounded w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block mb-1">Icon (FontAwesome CSS-Klasse)</label>
|
||||||
|
<input type="text" name="icon_class" class="p-2 border rounded w-full" placeholder="z.B. fas fa-user">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600">
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Tabelle der vorhandenen Bookmarks -->
|
||||||
|
<ul class="mt-4 space-y-2">
|
||||||
|
{% for bm in bookmarks %}
|
||||||
|
<li class="bg-gray-100 dark:bg-gray-700 p-2 rounded flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<i class="{{ bm.icon_class }} mr-2"></i>
|
||||||
|
<a href="{{ bm.url }}" target="_blank" class="text-blue-600 hover:underline">{{ bm.title }}</a>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ url_for('delete_bookmark', bookmark_id=bm.id, user_id=single_user.id) }}" onsubmit="return confirm('Lesezeichen wirklich löschen?')">
|
||||||
|
<button class="text-red-500 hover:text-red-700"><i class="fas fa-trash-alt"></i></button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<a href="{{ url_for('dashboard') }}" class="inline-block bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
<a href="{{ url_for('dashboard') }}" class="inline-block bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
|
||||||
Zurück zum Dashboard
|
Zurück zum Dashboard
|
||||||
</a>
|
</a>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
{% endblock content %}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
|
||||||
|
|
||||||
<!-- Tailwind Dark-Mode-Konfiguration -->
|
<!-- Dark-Mode-Einstellung -->
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
.dark .glassmorphism {
|
.dark .glassmorphism {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark input[type="text"],
|
.dark input[type="text"],
|
||||||
.dark input[type="email"],
|
.dark input[type="email"],
|
||||||
.dark input[type="password"],
|
.dark input[type="password"],
|
||||||
@@ -51,7 +50,13 @@
|
|||||||
background-color: #374151;
|
background-color: #374151;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
/* Suchfeld ohne sichtbaren Hintergrund oder Umrandung */
|
||||||
|
.search-input {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
/* Dock-Icons */
|
||||||
.dock-icon {
|
.dock-icon {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
@@ -61,12 +66,12 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-100 transition-colors duration-300"
|
<body class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-100 transition-colors duration-300"
|
||||||
style="background-image: url('{{ url_for('static', filename='24.png') }}');">
|
style="background-image: url('{{ url_for('static', filename='1.png') }}');">
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Dark mode per localStorage
|
// Dark Mode init
|
||||||
if (localStorage.getItem('darkMode') === 'true' ||
|
if (localStorage.getItem('darkMode') === 'true' ||
|
||||||
(!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
(!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<!-- templates/dashboard.html -->
|
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Dashboard{% endblock %}
|
{% block title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
@@ -23,16 +22,17 @@
|
|||||||
<div id="clock" class="text-xl sm:text-2xl font-semibold"></div>
|
<div id="clock" class="text-xl sm:text-2xl font-semibold"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Websuche unter dem Datum -->
|
<!-- Websuche: ohne Input-BG/Border -->
|
||||||
<div class="glassmorphism p-4 shadow-lg mb-6 w-full max-w-lg">
|
<div class="glassmorphism p-4 shadow-lg mb-6 w-full max-w-lg">
|
||||||
<form id="searchForm" class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 items-center justify-center">
|
<form id="searchForm" style="background-color: transparent !important;" class="flex flex-col sm:flex-row items-center justify-center">
|
||||||
<input type="text" id="searchInput" placeholder="Suche..."
|
<input type="text" style="background-color: transparent !important;" id="searchInput" placeholder="Suche..."
|
||||||
class="flex-grow p-2 border rounded text-gray-800" />
|
class="flex-grow bg-transparent border-0 outline-none placeholder-gray-500 text-gray-800
|
||||||
<select id="searchEngine" class="p-2 border rounded text-gray-800">
|
dark:placeholder-gray-300" />
|
||||||
|
<select id="searchEngine" style="background-color: transparent !important;" class="bg-transparent border-0 outline-none text-gray-800 dark:text-white ml-2">
|
||||||
<option value="qwant">Qwant</option>
|
<option value="qwant">Qwant</option>
|
||||||
<option value="google">Google</option>
|
<option value="google">Google</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
|
<button type="submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 ml-2">
|
||||||
Los
|
Los
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<i class="fas fa-home text-white text-xl"></i>
|
<i class="fas fa-home text-white text-xl"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Falls du trotzdem einen "Support" willst, kannst du es hier lassen -->
|
<!-- Support-Button (öffnet Support-Modal V1) -->
|
||||||
<button class="dock-icon w-12 h-12 flex items-center justify-center bg-purple-500 hover:bg-purple-600 rounded-xl
|
<button class="dock-icon w-12 h-12 flex items-center justify-center bg-purple-500 hover:bg-purple-600 rounded-xl
|
||||||
transition-colors duration-200" data-modal="supportModal">
|
transition-colors duration-200" data-modal="supportModal">
|
||||||
<i class="fas fa-question-circle text-white text-xl"></i>
|
<i class="fas fa-question-circle text-white text-xl"></i>
|
||||||
@@ -152,9 +152,17 @@
|
|||||||
<i class="fas fa-user-shield text-white text-xl"></i>
|
<i class="fas fa-user-shield text-white text-xl"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Einstellungen (öffnet Settings-Modal V1) -->
|
||||||
|
<button class="dock-icon w-12 h-12 flex items-center justify-center bg-yellow-700 hover:bg-yellow-800 rounded-xl
|
||||||
|
transition-colors duration-200" data-modal="settingsModal">
|
||||||
|
<i class="fas fa-cog text-white text-xl"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modals (z.B. Home, Support usw.) -->
|
<!-- =========================== MODALS =========================== -->
|
||||||
|
|
||||||
|
<!-- Home Modal -->
|
||||||
<div id="homeModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
<div id="homeModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||||
<div class="glassmorphism bg-white dark:bg-gray-800 p-6 rounded max-w-md w-full mx-auto">
|
<div class="glassmorphism bg-white dark:bg-gray-800 p-6 rounded max-w-md w-full mx-auto">
|
||||||
<h2 class="text-2xl font-bold mb-4">Willkommen Zuhause!</h2>
|
<h2 class="text-2xl font-bold mb-4">Willkommen Zuhause!</h2>
|
||||||
@@ -172,13 +180,78 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Support Modal (aus V1, mit ProblemType/E-Mail/Nachricht + sendSupportMessage) -->
|
||||||
<div id="supportModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
<div id="supportModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||||
<div class="glassmorphism bg-white dark:bg-gray-800 p-6 rounded max-w-md w-full mx-auto">
|
<div class="glassmorphism bg-white dark:bg-gray-800 p-8 rounded-lg max-w-md w-full mx-auto shadow-lg">
|
||||||
<h2 class="text-2xl font-bold mb-4">Support</h2>
|
<h2 class="text-2xl font-bold mb-4">Support kontaktieren</h2>
|
||||||
<p class="mb-4">Beschreiben Sie Ihr Problem</p>
|
|
||||||
<!-- ... -->
|
<!-- Dropdown zur Kategorisierung des Problems -->
|
||||||
<button class="mt-4 bg-gray-300 dark:bg-gray-700 hover:bg-gray-400 dark:hover:bg-gray-800 text-gray-800 dark:text-white
|
<label for="problemType" class="block mb-2 text-lg font-semibold">Art des Problems</label>
|
||||||
font-bold py-2 px-4 rounded close-modal">
|
<select id="problemType" class="w-full p-2 border rounded mb-4 text-gray-800 dark:bg-gray-700 dark:text-white">
|
||||||
|
<option value="Technisches Problem">Technisches Problem</option>
|
||||||
|
<option value="Account Problem">Account Problem</option>
|
||||||
|
<option value="Sonstiges">Sonstiges</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Eingabefeld für E-Mail -->
|
||||||
|
<label for="emailInput" class="block mb-2 text-lg font-semibold">Ihre E-Mail</label>
|
||||||
|
<input type="email" id="emailInput" class="w-full p-2 border rounded mb-4 text-gray-800 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="Ihre E-Mail-Adresse" />
|
||||||
|
|
||||||
|
<!-- Eingabefeld für Nachricht -->
|
||||||
|
<label for="messageInput" class="block mb-2 text-lg font-semibold">Nachricht</label>
|
||||||
|
<textarea id="messageInput" class="w-full p-2 border rounded mb-4 text-gray-800 dark:bg-gray-700 dark:text-white" rows="4"
|
||||||
|
placeholder="Beschreiben Sie Ihr Problem"></textarea>
|
||||||
|
|
||||||
|
<!-- Button zum Senden der Nachricht -->
|
||||||
|
<button id="sendSupportMessage" class="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600 transition-colors duration-200">
|
||||||
|
Nachricht senden
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Schließen-Button -->
|
||||||
|
<button class="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors duration-200 close-modal">
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Modal (aus V1, mit Wallpaper-Auswahl, Stadt, etc.) -->
|
||||||
|
<div id="settingsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center overflow-auto z-50">
|
||||||
|
<div class="glassmorphism bg-white dark:bg-gray-800 p-6 rounded-lg w-full max-w-md max-h-screen overflow-y-auto mx-4 shadow-lg">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">Einstellungen</h2>
|
||||||
|
<p>Hier können Sie Ihre Einstellungen anpassen und das System nach Ihren Wünschen konfigurieren.</p>
|
||||||
|
|
||||||
|
<!-- Hintergrundbild auswählen -->
|
||||||
|
<h3 class="text-lg font-semibold mb-2 mt-4">Hintergrundbild auswählen</h3>
|
||||||
|
<div id="wallpaperSelection" class="grid grid-cols-2 sm:grid-cols-3 gap-2 mb-4">
|
||||||
|
{% for i in range(1, 27) %}
|
||||||
|
<img src="{{ url_for('static', filename=i ~ '.png') }}" alt="Wallpaper {{ i }}"
|
||||||
|
class="w-full h-auto wallpaper-thumb cursor-pointer border-2
|
||||||
|
{% if wallpaper == i ~ '.png' %}border-blue-500{% else %}border-transparent{% endif %}
|
||||||
|
rounded"
|
||||||
|
data-wallpaper="{{ i }}.png">
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stadt ändern -->
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Stadt ändern</h3>
|
||||||
|
<input type="text" id="cityInput" class="w-full p-2 border rounded mb-4 text-gray-800 dark:bg-gray-700 dark:text-white"
|
||||||
|
placeholder="Ihre Stadt" value="{{ city }}" />
|
||||||
|
|
||||||
|
<!-- Unsichtbarer Wetter-Toggle (V1-Fallback) -->
|
||||||
|
<div style="display: none;">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Wochenvorhersage anzeigen</h3>
|
||||||
|
<label class="inline-flex items-center mt-2">
|
||||||
|
<input type="checkbox" id="weatherForecastToggle" class="form-checkbox h-5 w-5 text-blue-600"
|
||||||
|
{% if show_forecast %}checked{% endif %} />
|
||||||
|
<span class="ml-2 text-gray-700 dark:text-gray-300">Wochenvorhersage anzeigen</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="saveSettings" class="mt-4 bg-yellow-500 text-white px-4 py-2 rounded hover:bg-yellow-600 transition-colors duration-200">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button class="mt-4 ml-2 bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600 transition-colors duration-200 close-modal">
|
||||||
Schließen
|
Schließen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,7 +300,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modals (Home, Support)
|
// Modals (Home, Support, Settings)
|
||||||
const modals = document.querySelectorAll('[id$="Modal"]');
|
const modals = document.querySelectorAll('[id$="Modal"]');
|
||||||
const modalTriggers = document.querySelectorAll('[data-modal]');
|
const modalTriggers = document.querySelectorAll('[data-modal]');
|
||||||
const closeModalButtons = document.querySelectorAll('.close-modal');
|
const closeModalButtons = document.querySelectorAll('.close-modal');
|
||||||
@@ -256,7 +329,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Websuche
|
// Websuche (inline)
|
||||||
const searchForm = document.getElementById('searchForm');
|
const searchForm = document.getElementById('searchForm');
|
||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
const searchEngine = document.getElementById('searchEngine');
|
const searchEngine = document.getElementById('searchEngine');
|
||||||
@@ -272,5 +345,82 @@
|
|||||||
}
|
}
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Support Modal: Nachricht senden (aus V1)
|
||||||
|
const sendSupportMessage = document.getElementById('sendSupportMessage');
|
||||||
|
if (sendSupportMessage) {
|
||||||
|
sendSupportMessage.addEventListener('click', () => {
|
||||||
|
const email = document.getElementById('emailInput').value.trim();
|
||||||
|
const problemType = document.getElementById('problemType').value;
|
||||||
|
const message = document.getElementById('messageInput').value.trim();
|
||||||
|
if (email && message) {
|
||||||
|
fetch('/send_support_message', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, problemType, message })
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
alert('Ihre Nachricht wurde erfolgreich gesendet.');
|
||||||
|
const modal = document.getElementById('supportModal');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Senden der Nachricht.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Bitte E-Mail und Nachricht ausfüllen.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings Modal (Wallpaper, Stadt, etc.) aus V1
|
||||||
|
// Lese/Schreibe Settings per '/get_settings' und '/save_settings'
|
||||||
|
let selectedWallpaper = '{{ wallpaper }}';
|
||||||
|
|
||||||
|
// Markiere ausgewähltes Wallpaper
|
||||||
|
const wallpaperThumbs = document.querySelectorAll('.wallpaper-thumb');
|
||||||
|
wallpaperThumbs.forEach(thumb => {
|
||||||
|
thumb.addEventListener('click', function() {
|
||||||
|
wallpaperThumbs.forEach(t => t.classList.remove('border-blue-500'));
|
||||||
|
wallpaperThumbs.forEach(t => t.classList.add('border-transparent'));
|
||||||
|
this.classList.remove('border-transparent');
|
||||||
|
this.classList.add('border-blue-500');
|
||||||
|
selectedWallpaper = this.getAttribute('data-wallpaper');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveSettingsBtn = document.getElementById('saveSettings');
|
||||||
|
if (saveSettingsBtn) {
|
||||||
|
saveSettingsBtn.addEventListener('click', () => {
|
||||||
|
const city = document.getElementById('cityInput').value.trim();
|
||||||
|
const showForecast = document.getElementById('weatherForecastToggle').checked;
|
||||||
|
// Alte Settings laden
|
||||||
|
fetch('/get_settings')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(settings => {
|
||||||
|
const bookmarks = settings.bookmarks || [];
|
||||||
|
// Neue Settings absenden
|
||||||
|
fetch('/save_settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
wallpaper: selectedWallpaper,
|
||||||
|
city: city,
|
||||||
|
show_forecast: showForecast,
|
||||||
|
bookmarks: bookmarks
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Speichern der Einstellungen.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
33
Dashboard_V2/templates/reset_password.html
Normal file
33
Dashboard_V2/templates/reset_password.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Passwort zurücksetzen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Neues Passwort setzen</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, msg in messages %}
|
||||||
|
<div class="mb-2 bg-{{category}}-500 text-white p-2 rounded">
|
||||||
|
{{ msg }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('reset_password', token=token) }}"
|
||||||
|
class="max-w-md glassmorphism p-4">
|
||||||
|
<label class="block mb-2" for="pw1">Neues Passwort</label>
|
||||||
|
<input type="password" name="pw1" id="pw1"
|
||||||
|
class="w-full p-2 border rounded text-gray-800 mb-4" required>
|
||||||
|
|
||||||
|
<label class="block mb-2" for="pw2">Passwort bestätigen</label>
|
||||||
|
<input type="password" name="pw2" id="pw2"
|
||||||
|
class="w-full p-2 border rounded text-gray-800 mb-4" required>
|
||||||
|
|
||||||
|
<button type="submit" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded">
|
||||||
|
Passwort ändern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
Reference in New Issue
Block a user