Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8ad3aea13 | |||
| edf3049e42 | |||
| d117978005 | |||
| 48d8463481 | |||
| 08314ec703 | |||
| 0bb7d8d0dc | |||
| 4a28c2c453 | |||
| 66d987857a | |||
| d58aba26c2 | |||
| 8f0a6d4372 | |||
| 5372fe220e | |||
| 11ab15127c | |||
| 0705ecce59 | |||
| 1c59b0b616 | |||
| d42c43db50 | |||
| e46264b201 | |||
| 74307ba345 | |||
| 14474c4eab | |||
| 4797cc3b72 | |||
| ab280b55af | |||
| 84b492d8d2 | |||
| b0db3398f2 |
13
.env
Normal file
13
.env
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# MindMap Umgebungsvariablen
|
||||||
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||||
|
|
||||||
|
# OpenAI API
|
||||||
|
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
|
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db OPENAI_API_KEY=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
|
||||||
10
Dockerfile
10
Dockerfile
@@ -1,10 +0,0 @@
|
|||||||
FROM python:3.9-slim-buster
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
|
|
||||||
COPY website .
|
|
||||||
|
|
||||||
CMD ["python", "app.py"]
|
|
||||||
133
README.md
133
README.md
@@ -1 +1,134 @@
|
|||||||
|
# MindMapProjekt - Roadmap
|
||||||
|
|
||||||
|
## Projektübersicht
|
||||||
|
Das MindMapProjekt ist eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen. Das Projekt wird umfassend überarbeitet, um ein modernes, benutzerfreundliches Design und erweiterte Funktionalitäten zu bieten.
|
||||||
|
|
||||||
|
## Technischer Stack
|
||||||
|
- **Backend**: Python/Flask
|
||||||
|
- **Frontend**:
|
||||||
|
- Tailwind CSS für moderne UI
|
||||||
|
- SVG-Bibliotheken für Visualisierungen (D3.js)
|
||||||
|
- JavaScript/Alpine.js für interaktive Komponenten
|
||||||
|
- **Datenbank**: SQLite mit SQLAlchemy
|
||||||
|
- **KI-Integration**: OpenAI API für intelligente Assistenz
|
||||||
|
|
||||||
|
## Installation und Verwendung
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. Repository klonen
|
||||||
|
2. Virtuelle Umgebung erstellen: `python -m venv venv`
|
||||||
|
3. Virtuelle Umgebung aktivieren:
|
||||||
|
- Windows: `venv\Scripts\activate`
|
||||||
|
- Unix/MacOS: `source venv/bin/activate`
|
||||||
|
4. Abhängigkeiten installieren: `pip install -r requirements.txt`
|
||||||
|
5. Datenbank initialisieren: `python TOOLS.py db:rebuild`
|
||||||
|
6. Admin-Benutzer erstellen: `python TOOLS.py user:admin`
|
||||||
|
7. Server starten: `python TOOLS.py server:run`
|
||||||
|
|
||||||
|
### Standardbenutzer
|
||||||
|
- **Admin-Benutzer**: Username: `admin` / Passwort: `admin`
|
||||||
|
- **Testbenutzer**: Username: `user` / Passwort: `user`
|
||||||
|
|
||||||
|
### Verwaltungswerkzeuge mit TOOLS.py
|
||||||
|
Das Projekt enthält ein zentrales Verwaltungsskript `TOOLS.py`, das verschiedene Hilfsfunktionen bietet:
|
||||||
|
|
||||||
|
#### Datenbankverwaltung
|
||||||
|
- `python TOOLS.py db:fix` - Reparieren der Datenbankstruktur
|
||||||
|
- `python TOOLS.py db:rebuild` - Datenbank neu aufbauen (löscht alle Daten!)
|
||||||
|
- `python TOOLS.py db:test` - Datenbankverbindung und Modelle testen
|
||||||
|
- `python TOOLS.py db:stats` - Datenbankstatistiken anzeigen
|
||||||
|
|
||||||
|
#### Benutzerverwaltung
|
||||||
|
- `python TOOLS.py user:list` - Alle Benutzer anzeigen
|
||||||
|
- `python TOOLS.py user:create -u USERNAME -e EMAIL -p PASSWORD [-a]` - Neuen Benutzer erstellen
|
||||||
|
- `python TOOLS.py user:admin` - Admin-Benutzer erstellen (admin/admin)
|
||||||
|
- `python TOOLS.py user:reset-pw -u USERNAME -p NEWPASSWORD` - Benutzerpasswort zurücksetzen
|
||||||
|
- `python TOOLS.py user:delete -u USERNAME` - Benutzer löschen
|
||||||
|
|
||||||
|
#### Serververwaltung
|
||||||
|
- `python TOOLS.py server:run [--host HOST] [--port PORT] [--no-debug]` - Entwicklungsserver starten
|
||||||
|
|
||||||
|
Für detaillierte Hilfe: `python TOOLS.py -h`
|
||||||
|
|
||||||
|
## Roadmap der Überarbeitung
|
||||||
|
|
||||||
|
### Phase 1: Grundlegende Infrastruktur ✅
|
||||||
|
- [x] Bestandsaufnahme des aktuellen Projekts
|
||||||
|
- [x] Erstellung der Roadmap
|
||||||
|
- [x] Aktualisierung der Abhängigkeiten
|
||||||
|
- [x] Integration von Tailwind CSS
|
||||||
|
- [x] Einrichtung der SVG-Bibliotheken (D3.js)
|
||||||
|
- [x] Favicon erstellen
|
||||||
|
- [x] Setup-Skript für einfache Installation
|
||||||
|
|
||||||
|
### Phase 2: Design-Überarbeitung 🔄
|
||||||
|
- [x] Implementierung des Dark Mode
|
||||||
|
- [x] Erstellung eines modernen, minimalistischen UI mit Tech-Ästhetik
|
||||||
|
- [x] Responsive Design für alle Geräte
|
||||||
|
- [ ] Gestaltung der Landing Page mit großer Typografie
|
||||||
|
|
||||||
|
### Phase 3: Mindmap-Funktionalitäten 🔄
|
||||||
|
- [x] Verbesserte Visualisierung mit SVG und D3.js
|
||||||
|
- [x] Implementierung der Mouseover-Funktion
|
||||||
|
- [x] Entwicklung der Suchfunktion für Knoten
|
||||||
|
- [ ] Tagging-System für Inhalte
|
||||||
|
- [ ] Quellenmanagement und -verlinkung
|
||||||
|
- [ ] Upload-Funktionalität an Knotenpunkten
|
||||||
|
|
||||||
|
### Phase 4: Kernseitenentwicklung
|
||||||
|
- [ ] Überarbeitung der Startseite mit neuen Features
|
||||||
|
- [ ] Entwicklung der "Wer sind wir?"-Seite
|
||||||
|
- [ ] Implementierung von Impressum und Datenschutzerklärung
|
||||||
|
- [ ] Erstellung der Kontaktseite mit FAQs
|
||||||
|
- [ ] Überarbeitung des Benutzerprofilbereichs
|
||||||
|
|
||||||
|
### Phase 5: Community-Features
|
||||||
|
- [ ] Entwicklung des Autorenbereichs
|
||||||
|
- [ ] Implementierung von Community-Bereichen für Themenbereiche
|
||||||
|
- [ ] Verbesserter Kommentarbereich
|
||||||
|
- [ ] Benutzerrechtemanagement
|
||||||
|
|
||||||
|
### Phase 6: KI-Integration
|
||||||
|
- [ ] Implementierung des Frage-Antwort-Systems
|
||||||
|
- [ ] KI-generierte Themeneinleitungen
|
||||||
|
- [ ] Intelligente Suchunterstützung
|
||||||
|
- [ ] Geführte Pfade durch Themenbereiche
|
||||||
|
- [ ] Vorgeschlagene Chat-Möglichkeiten
|
||||||
|
|
||||||
|
### Phase 7: Benutzerprofilfunktionen
|
||||||
|
- [ ] Speichern von Thematiken
|
||||||
|
- [ ] Persönliche Mindmap/Pinboard
|
||||||
|
- [ ] Beitragsmanagement
|
||||||
|
- [ ] Benutzerstatistiken und -aktivitäten
|
||||||
|
|
||||||
|
### Phase 8: Testing und Optimierung
|
||||||
|
- [ ] Umfassende Tests aller Funktionen
|
||||||
|
- [ ] Performance-Optimierung
|
||||||
|
- [ ] SEO-Implementierung
|
||||||
|
- [ ] Barrierefreiheit prüfen und verbessern
|
||||||
|
|
||||||
|
### Phase 9: Dokumentation und Einführung
|
||||||
|
- [ ] Erstellung von Benutzeranleitungen
|
||||||
|
- [ ] Entwicklerdokumentation
|
||||||
|
- [ ] Administratorenhandbuch
|
||||||
|
- [ ] Guided Tour für neue Benutzer
|
||||||
|
|
||||||
|
## Aktueller Status
|
||||||
|
- **Phase 1**: ✅ Abgeschlossen
|
||||||
|
- **Phase 2**: 🔄 In Bearbeitung (75% abgeschlossen)
|
||||||
|
- **Phase 3**: 🔄 In Bearbeitung (50% abgeschlossen)
|
||||||
|
|
||||||
|
## Aktuelle Fortschritte
|
||||||
|
- Grundlegende UI modernisiert mit Tailwind CSS und Dark Mode
|
||||||
|
- Neues Favicon für bessere visuelle Identität erstellt
|
||||||
|
- Setup-Prozess vereinfacht mit einem Shell-Skript
|
||||||
|
- Mindmap-Visualisierung komplett überarbeitet mit D3.js für eine interaktivere Erfahrung
|
||||||
|
- Responsive Design für optimale Darstellung auf allen Geräten
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
- Fertigstellung der Landing Page
|
||||||
|
- Erstellung der "Wer sind wir?"-Seite
|
||||||
|
- Implementierung des Tagging-Systems für Gedanken
|
||||||
|
- Verbesserung der Gedankenansicht im Mindmap-Bereich
|
||||||
|
|
||||||
|
*Zuletzt aktualisiert: 01.06.2024*
|
||||||
102
ROADMAP.md
Normal file
102
ROADMAP.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Systades Mindmap - Entwicklungs-Roadmap
|
||||||
|
|
||||||
|
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
|
||||||
|
|
||||||
|
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen)
|
||||||
|
|
||||||
|
- [x] Entwurf des Datenbankschemas für benutzerorientierte Mindmaps
|
||||||
|
- [x] Implementierung der Modelle in models.py
|
||||||
|
- [x] Erstellung der API-Endpunkte für CRUD-Operationen
|
||||||
|
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
||||||
|
- [x] Seed-Daten für die Entwicklung und Tests
|
||||||
|
|
||||||
|
## Phase 2: Dynamische Mindmap-Visualisierung (Aktuell)
|
||||||
|
|
||||||
|
- [ ] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
|
||||||
|
- [ ] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
||||||
|
- [ ] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
||||||
|
- [ ] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
||||||
|
- [ ] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
||||||
|
|
||||||
|
## Phase 3: Benutzerdefinierte Mindmaps
|
||||||
|
|
||||||
|
- [ ] UI für das Erstellen, Bearbeiten und Löschen eigener Mindmaps
|
||||||
|
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
||||||
|
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
||||||
|
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
||||||
|
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
||||||
|
|
||||||
|
## Phase 4: Notizen und Annotationen
|
||||||
|
|
||||||
|
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
||||||
|
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
||||||
|
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
||||||
|
- [ ] Kategorisierung und Farbkodierung von Notizen
|
||||||
|
- [ ] Suchfunktion für Notizen
|
||||||
|
|
||||||
|
## Phase 5: Integrationen und Erweiterungen
|
||||||
|
|
||||||
|
- [ ] Import/Export-Funktionalität für Mindmaps (JSON, PNG)
|
||||||
|
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
||||||
|
- [ ] Kollaborative Bearbeitung von Mindmaps
|
||||||
|
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
||||||
|
- [ ] Versionierung von Mindmaps
|
||||||
|
|
||||||
|
## Phase 6: KI-Integration und Analyse
|
||||||
|
|
||||||
|
- [ ] KI-gestützte Vorschläge für Verbindungen zwischen Knoten
|
||||||
|
- [ ] Automatische Kategorisierung von Inhalten
|
||||||
|
- [ ] Visualisierung von Beziehungsstärken und -typen
|
||||||
|
- [ ] Mindmap-Statistiken und Analysen
|
||||||
|
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
||||||
|
|
||||||
|
## Phase 7: Optimierung und Skalierung
|
||||||
|
|
||||||
|
- [ ] Performance-Optimierung für große Mindmaps
|
||||||
|
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
||||||
|
- [ ] Erweiterte Such- und Filterfunktionen
|
||||||
|
- [ ] Mobile Optimierung
|
||||||
|
- [ ] Offline-Funktionalität mit Synchronisierung
|
||||||
|
|
||||||
|
## Technische Schulden und Refactoring
|
||||||
|
|
||||||
|
- [ ] Trennung der Datenbank-Logik vom Flask-App-Code
|
||||||
|
- [ ] Einführung von Unit-Tests und Integration-Tests
|
||||||
|
- [ ] Überarbeitung der API-Dokumentation
|
||||||
|
- [ ] Caching-Strategien für bessere Performance
|
||||||
|
- [ ] Verbesserte Fehlerbehandlung und Logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementierungsdetails
|
||||||
|
|
||||||
|
### Datenbankschema
|
||||||
|
|
||||||
|
Das Datenbankschema umfasst folgende Hauptentitäten:
|
||||||
|
|
||||||
|
1. **Category** - Wissenschaftliche Kategorien für die öffentliche Mindmap
|
||||||
|
2. **MindMapNode** - Öffentliche Mindmap-Knoten mit Metadaten
|
||||||
|
3. **UserMindmap** - Benutzerdefinierte Mindmaps
|
||||||
|
4. **UserMindmapNode** - Verknüpfung zwischen Benutzermindmaps und öffentlichen Knoten
|
||||||
|
5. **MindmapNote** - Benutzerspezifische Notizen
|
||||||
|
6. **Thought** - Gedanken und Inhalte, die Knoten zugeordnet sind
|
||||||
|
7. **ThoughtRelation** - Beziehungen zwischen Gedanken
|
||||||
|
|
||||||
|
### Frontend-Technologien
|
||||||
|
|
||||||
|
- D3.js für die Visualisierung der Mindmap
|
||||||
|
- AJAX für dynamisches Laden von Daten
|
||||||
|
- Interaktive Bedienelemente mit JavaScript
|
||||||
|
- Responsive Design mit Tailwind CSS
|
||||||
|
|
||||||
|
### Backend-APIs
|
||||||
|
|
||||||
|
Die implementierten API-Endpunkte umfassen:
|
||||||
|
|
||||||
|
- `/api/mindmap/public` - Abrufen der öffentlichen Mindmap-Struktur
|
||||||
|
- `/api/mindmap/user/<id>` - Abrufen benutzerdefinierter Mindmaps
|
||||||
|
- `/api/mindmap/<id>/add_node` - Hinzufügen eines Knotens zur Benutzer-Mindmap
|
||||||
|
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
||||||
|
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
||||||
|
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
||||||
|
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
||||||
125
TOOLS.py
Executable file
125
TOOLS.py
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
TOOLS.py - Main utility script for the website application.
|
||||||
|
|
||||||
|
This script provides a command-line interface to all utilities
|
||||||
|
for database management, user management, and server administration.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 TOOLS.py [command] [options]
|
||||||
|
|
||||||
|
Available commands:
|
||||||
|
- db:fix Fix database schema
|
||||||
|
- db:rebuild Completely rebuild the database
|
||||||
|
- db:test Test database connection and models
|
||||||
|
- db:stats Show database statistics
|
||||||
|
|
||||||
|
- user:list List all users
|
||||||
|
- user:create Create a new user
|
||||||
|
- user:admin Create admin user (username: admin, password: admin)
|
||||||
|
- user:reset-pw Reset user password
|
||||||
|
- user:delete Delete a user
|
||||||
|
|
||||||
|
- server:run Run the development server
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python3 TOOLS.py db:rebuild
|
||||||
|
python3 TOOLS.py user:admin
|
||||||
|
python3 TOOLS.py server:run --port 8080
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from utils import (
|
||||||
|
fix_database_schema, rebuild_database, run_all_tests, print_database_stats,
|
||||||
|
list_users, create_user, reset_password, delete_user, create_admin_user,
|
||||||
|
run_development_server
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Website Administration Tools',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=__doc__
|
||||||
|
)
|
||||||
|
|
||||||
|
# Main command argument
|
||||||
|
parser.add_argument('command', help='Command to execute')
|
||||||
|
|
||||||
|
# Additional arguments
|
||||||
|
parser.add_argument('--username', '-u', help='Username for user commands')
|
||||||
|
parser.add_argument('--email', '-e', help='Email for user creation')
|
||||||
|
parser.add_argument('--password', '-p', help='Password for user creation/reset')
|
||||||
|
parser.add_argument('--admin', '-a', action='store_true', help='Make user an admin')
|
||||||
|
parser.add_argument('--host', help='Host for server (default: 127.0.0.1)')
|
||||||
|
parser.add_argument('--port', type=int, help='Port for server (default: 5000)')
|
||||||
|
parser.add_argument('--no-debug', action='store_true', help='Disable debug mode for server')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
# Database commands
|
||||||
|
if args.command == 'db:fix':
|
||||||
|
fix_database_schema()
|
||||||
|
|
||||||
|
elif args.command == 'db:rebuild':
|
||||||
|
print("WARNING: This will delete all data in the database!")
|
||||||
|
confirm = input("Are you sure you want to continue? (y/n): ").lower()
|
||||||
|
if confirm == 'y':
|
||||||
|
rebuild_database()
|
||||||
|
else:
|
||||||
|
print("Aborted.")
|
||||||
|
|
||||||
|
elif args.command == 'db:test':
|
||||||
|
run_all_tests()
|
||||||
|
|
||||||
|
elif args.command == 'db:stats':
|
||||||
|
print_database_stats()
|
||||||
|
|
||||||
|
# User commands
|
||||||
|
elif args.command == 'user:list':
|
||||||
|
list_users()
|
||||||
|
|
||||||
|
elif args.command == 'user:create':
|
||||||
|
if not args.username or not args.email or not args.password:
|
||||||
|
print("Error: Username, email, and password are required.")
|
||||||
|
print("Example: python3 TOOLS.py user:create -u username -e email -p password [-a]")
|
||||||
|
sys.exit(1)
|
||||||
|
create_user(args.username, args.email, args.password, args.admin)
|
||||||
|
|
||||||
|
elif args.command == 'user:admin':
|
||||||
|
create_admin_user()
|
||||||
|
|
||||||
|
elif args.command == 'user:reset-pw':
|
||||||
|
if not args.username or not args.password:
|
||||||
|
print("Error: Username and password are required.")
|
||||||
|
print("Example: python3 TOOLS.py user:reset-pw -u username -p new_password")
|
||||||
|
sys.exit(1)
|
||||||
|
reset_password(args.username, args.password)
|
||||||
|
|
||||||
|
elif args.command == 'user:delete':
|
||||||
|
if not args.username:
|
||||||
|
print("Error: Username is required.")
|
||||||
|
print("Example: python3 TOOLS.py user:delete -u username")
|
||||||
|
sys.exit(1)
|
||||||
|
delete_user(args.username)
|
||||||
|
|
||||||
|
# Server commands
|
||||||
|
elif args.command == 'server:run':
|
||||||
|
host = args.host or '127.0.0.1'
|
||||||
|
port = args.port or 5000
|
||||||
|
debug = not args.no_debug
|
||||||
|
run_development_server(host=host, port=port, debug=debug)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {args.command}")
|
||||||
|
print("Run 'python3 TOOLS.py -h' for usage information")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
BIN
__pycache__/app.cpython-311.pyc
Normal file
BIN
__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/models.cpython-311.pyc
Normal file
BIN
__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
4
cookies.txt
Normal file
4
cookies.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# https://curl.se/docs/http-cookies.html
|
||||||
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
BIN
database/systades.db
Normal file
BIN
database/systades.db
Normal file
Binary file not shown.
BIN
database/systades.db.backup
Normal file
BIN
database/systades.db.backup
Normal file
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
restart: always
|
|
||||||
69
docs/ANLEITUNG.md
Normal file
69
docs/ANLEITUNG.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Anleitung: Animierter Netzwerk-Hintergrund
|
||||||
|
|
||||||
|
Diese Anleitung erklärt, wie Sie ein Netzwerk-Bild als animierten Hintergrund für die gesamte Website einrichten können.
|
||||||
|
|
||||||
|
## Option 1: Manuelle Installation
|
||||||
|
|
||||||
|
1. Kopieren Sie das gewünschte Netzwerk-Bild (z.B. `d2efd014-1325-471f-b9a7-90d025eb81d6.png`) in die Datei `website/static/network-bg.jpg`.
|
||||||
|
|
||||||
|
Sie können dafür das beiliegende Batch-Skript verwenden:
|
||||||
|
```
|
||||||
|
copy-network-image.bat d2efd014-1325-471f-b9a7-90d025eb81d6.png
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Starten Sie den Flask-Server mit dem folgenden Befehl:
|
||||||
|
```
|
||||||
|
python start-flask-server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Öffnen Sie die Website unter http://127.0.0.1:5000/
|
||||||
|
|
||||||
|
## Anpassung der Animation
|
||||||
|
|
||||||
|
Sie können die Animation des Netzwerk-Hintergrunds anpassen, indem Sie die Datei `website/static/network-background.js` bearbeiten. Hier sind die wichtigsten Parameter:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let animationSpeed = 0.0005; // Geschwindigkeit der Rotation
|
||||||
|
let scaleSpeed = 0.0002; // Geschwindigkeit der Skalierung
|
||||||
|
let opacitySpeed = 0.0003; // Geschwindigkeit der Transparenzänderung
|
||||||
|
```
|
||||||
|
|
||||||
|
## Animation der Mindmap-Verbindungen
|
||||||
|
|
||||||
|
Die Verbindungen zwischen den Knoten in der Mindmap werden jetzt mit einer fließenden Animation dargestellt. Diese Animationen verbessern die Sichtbarkeit der Zusammenhänge und machen die Interaktion mit der Karte intuitiver.
|
||||||
|
|
||||||
|
### Funktionen:
|
||||||
|
|
||||||
|
1. **Animierte Linien**: Die Verbindungslinien zwischen den Knoten bewegen sich in einem fließenden Muster.
|
||||||
|
2. **Hervorhebung bei Hover**: Beim Überfahren eines Knotens oder einer Verbindung mit der Maus werden diese hervorgehoben.
|
||||||
|
3. **Kategorien-Beziehungen**: Die visuellen Verbindungen zwischen den Kategorien sind jetzt deutlicher erkennbar.
|
||||||
|
|
||||||
|
## Position des Auswahlfelds
|
||||||
|
|
||||||
|
Das Auswahlfeld auf der Karte wurde weiter nach links verschoben, sodass es vollständig sichtbar ist, wenn keine Auswahl getroffen wurde. Die Größe wurde ebenfalls angepasst, um die Lesbarkeit zu verbessern.
|
||||||
|
|
||||||
|
## Wiederherstellung des ursprünglichen Hintergrunds (optional)
|
||||||
|
|
||||||
|
Wenn Sie zum ursprünglichen Sternenhintergrund zurückkehren möchten, müssen Sie folgende Änderungen vornehmen:
|
||||||
|
|
||||||
|
1. Bearbeiten Sie die Datei `website/templates/base.html` und ersetzen Sie:
|
||||||
|
```html
|
||||||
|
<!-- Network Background Script -->
|
||||||
|
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
mit:
|
||||||
|
```html
|
||||||
|
<!-- Three.js für den Sternenhintergrund -->
|
||||||
|
<script src="{{ url_for('static', filename='three.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='background.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Bearbeiten Sie die Datei `website/templates/mindmap.html` und entfernen Sie die Zeile:
|
||||||
|
```html
|
||||||
|
<script src="{{ url_for('static', filename='network-animation.js') }}"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Entfernen Sie den CSS-Code für `#mindmap-container::before` und die anderen netzwerkspezifischen Stile aus der Datei `website/templates/mindmap.html`.
|
||||||
|
|
||||||
|
4. Starten Sie den Flask-Server neu, um die Änderungen zu übernehmen.
|
||||||
BIN
docs/Grundstruktur (funktionales Modell).pdf
Normal file
BIN
docs/Grundstruktur (funktionales Modell).pdf
Normal file
Binary file not shown.
13
example.env
Normal file
13
example.env
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# MindMap Umgebungsvariablen
|
||||||
|
# Kopiere diese Datei zu .env und passe die Werte an
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
SECRET_KEY=dein-geheimer-schluessel-hier
|
||||||
|
|
||||||
|
# OpenAI API
|
||||||
|
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
|
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
||||||
258
init_db.py
Executable file
258
init_db.py
Executable file
@@ -0,0 +1,258 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from app import app, initialize_database, db_path
|
||||||
|
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
|
||||||
|
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
|
||||||
|
import os
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""Initialisiert die Datenbank mit Beispieldaten."""
|
||||||
|
with app.app_context():
|
||||||
|
# Datenbank löschen und neu erstellen
|
||||||
|
if os.path.exists(db_path):
|
||||||
|
os.remove(db_path)
|
||||||
|
|
||||||
|
# Stellen Sie sicher, dass das Verzeichnis existiert
|
||||||
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Admin-Benutzer erstellen
|
||||||
|
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
||||||
|
admin.set_password('admin')
|
||||||
|
db.session.add(admin)
|
||||||
|
|
||||||
|
# Beispiel-Benutzer erstellen
|
||||||
|
user = User(username='user', email='user@example.com')
|
||||||
|
user.set_password('user')
|
||||||
|
db.session.add(user)
|
||||||
|
|
||||||
|
# Commit, um IDs zu generieren
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Wissenschaftliche Kategorien erstellen
|
||||||
|
science = Category(name='Wissenschaft', description='Wissenschaftliche Erkenntnisse',
|
||||||
|
color_code='#4CAF50', icon='flask')
|
||||||
|
db.session.add(science)
|
||||||
|
|
||||||
|
philosophy = Category(name='Philosophie', description='Philosophische Theorien und Gedanken',
|
||||||
|
color_code='#9C27B0', icon='lightbulb')
|
||||||
|
db.session.add(philosophy)
|
||||||
|
|
||||||
|
technology = Category(name='Technologie', description='Technologische Entwicklungen',
|
||||||
|
color_code='#FF9800', icon='microchip')
|
||||||
|
db.session.add(technology)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Wissenschaftliche Unterkategorien
|
||||||
|
physics = Category(name='Physik', description='Studium der Materie und Energie',
|
||||||
|
color_code='#81C784', icon='atom', parent_id=science.id)
|
||||||
|
biology = Category(name='Biologie', description='Studium lebender Organismen',
|
||||||
|
color_code='#66BB6A', icon='leaf', parent_id=science.id)
|
||||||
|
chemistry = Category(name='Chemie', description='Studium der Stoffe und ihrer Reaktionen',
|
||||||
|
color_code='#A5D6A7', icon='vial', parent_id=science.id)
|
||||||
|
|
||||||
|
db.session.add_all([physics, biology, chemistry])
|
||||||
|
|
||||||
|
# Technologie-Unterkategorien
|
||||||
|
informatics = Category(name='Informatik', description='Studium der Informationsverarbeitung',
|
||||||
|
color_code='#FFB74D', icon='laptop-code', parent_id=technology.id)
|
||||||
|
ai = Category(name='Künstliche Intelligenz', description='Entwicklung intelligenter Systeme',
|
||||||
|
color_code='#FFA726', icon='robot', parent_id=technology.id)
|
||||||
|
|
||||||
|
db.session.add_all([informatics, ai])
|
||||||
|
|
||||||
|
# Philosophie-Unterkategorien
|
||||||
|
ethics = Category(name='Ethik', description='Moralphilosophie und Wertesysteme',
|
||||||
|
color_code='#BA68C8', icon='balance-scale', parent_id=philosophy.id)
|
||||||
|
logic = Category(name='Logik', description='Studie der gültigen Schlussfolgerungen',
|
||||||
|
color_code='#AB47BC', icon='project-diagram', parent_id=philosophy.id)
|
||||||
|
|
||||||
|
db.session.add_all([ethics, logic])
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Knoten für die öffentliche Mindmap erstellen
|
||||||
|
nodes = {
|
||||||
|
'quantenmechanik': MindMapNode(
|
||||||
|
name='Quantenmechanik',
|
||||||
|
description='Physikalische Theorie zur Beschreibung der Materie auf atomarer Ebene',
|
||||||
|
color_code='#81C784',
|
||||||
|
category_id=physics.id,
|
||||||
|
created_by_id=admin.id
|
||||||
|
),
|
||||||
|
'relativitaetstheorie': MindMapNode(
|
||||||
|
name='Relativitätstheorie',
|
||||||
|
description='Einsteins Theorien zur Raumzeit und Gravitation',
|
||||||
|
color_code='#81C784',
|
||||||
|
category_id=physics.id,
|
||||||
|
created_by_id=admin.id
|
||||||
|
),
|
||||||
|
'genetik': MindMapNode(
|
||||||
|
name='Genetik',
|
||||||
|
description='Wissenschaft der Gene und Vererbung',
|
||||||
|
color_code='#66BB6A',
|
||||||
|
category_id=biology.id,
|
||||||
|
created_by_id=admin.id
|
||||||
|
),
|
||||||
|
'machine_learning': MindMapNode(
|
||||||
|
name='Machine Learning',
|
||||||
|
description='Algorithmen, die aus Daten lernen können',
|
||||||
|
color_code='#FFA726',
|
||||||
|
category_id=ai.id,
|
||||||
|
created_by_id=admin.id
|
||||||
|
),
|
||||||
|
'ki_ethik': MindMapNode(
|
||||||
|
name='KI-Ethik',
|
||||||
|
description='Moralische Implikationen künstlicher Intelligenz',
|
||||||
|
color_code='#BA68C8',
|
||||||
|
category_id=ethics.id,
|
||||||
|
created_by_id=user.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in nodes.values():
|
||||||
|
db.session.add(node)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Verknüpfungen zwischen Knoten herstellen (Hierarchie)
|
||||||
|
nodes['machine_learning'].parents.append(nodes['ki_ethik'])
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Gedanken erstellen
|
||||||
|
thoughts = [
|
||||||
|
{
|
||||||
|
'title': 'Künstliche Intelligenz und Bewusstsein',
|
||||||
|
'content': 'Die Frage nach maschinellem Bewusstsein ist fundamental für die KI-Ethik. Aktuelle KI-Systeme haben kein Bewusstsein, aber fortschrittliche KI könnte in Zukunft Eigenschaften entwickeln, die diesem nahekommen.',
|
||||||
|
'abstract': 'Eine Untersuchung der philosophischen Implikationen von KI-Bewusstsein.',
|
||||||
|
'keywords': 'KI, Bewusstsein, Ethik, Philosophie',
|
||||||
|
'branch': 'Philosophie',
|
||||||
|
'color_code': '#BA68C8',
|
||||||
|
'source_type': 'Markdown',
|
||||||
|
'user_id': user.id,
|
||||||
|
'node': nodes['ki_ethik']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Quantenmechanik und Realität',
|
||||||
|
'content': 'Die Kopenhagener Deutung und ihre Auswirkungen auf unser Verständnis der Realität. Quantenmechanik stellt grundlegende Annahmen über Determinismus und Lokalität in Frage.',
|
||||||
|
'abstract': 'Eine Analyse verschiedener Interpretationen der Quantenmechanik.',
|
||||||
|
'keywords': 'Quantenmechanik, Physik, Realität',
|
||||||
|
'branch': 'Physik',
|
||||||
|
'color_code': '#81C784',
|
||||||
|
'source_type': 'PDF',
|
||||||
|
'user_id': admin.id,
|
||||||
|
'node': nodes['quantenmechanik']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Deep Learning Fortschritte',
|
||||||
|
'content': 'Die neuesten Fortschritte im Deep Learning haben zu beeindruckenden Ergebnissen in Bereichen wie Computer Vision, Natural Language Processing und Reinforcement Learning geführt.',
|
||||||
|
'abstract': 'Überblick über aktuelle Deep Learning-Techniken und ihre Anwendungen.',
|
||||||
|
'keywords': 'Deep Learning, Neural Networks, AI',
|
||||||
|
'branch': 'Technologie',
|
||||||
|
'color_code': '#FFA726',
|
||||||
|
'source_type': 'Webpage',
|
||||||
|
'user_id': admin.id,
|
||||||
|
'node': nodes['machine_learning']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
thought_objects = []
|
||||||
|
for t_data in thoughts:
|
||||||
|
node = t_data.pop('node')
|
||||||
|
thought = Thought(**t_data)
|
||||||
|
node.thoughts.append(thought)
|
||||||
|
thought_objects.append(thought)
|
||||||
|
db.session.add(thought)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Beziehungen zwischen Gedanken
|
||||||
|
relation = ThoughtRelation(
|
||||||
|
source_id=thought_objects[0].id,
|
||||||
|
target_id=thought_objects[2].id,
|
||||||
|
relation_type=RelationType.INSPIRES,
|
||||||
|
created_by_id=user.id
|
||||||
|
)
|
||||||
|
db.session.add(relation)
|
||||||
|
|
||||||
|
# Bewertungen erstellen
|
||||||
|
rating1 = ThoughtRating(
|
||||||
|
thought_id=thought_objects[0].id,
|
||||||
|
user_id=admin.id,
|
||||||
|
relevance_score=5
|
||||||
|
)
|
||||||
|
rating2 = ThoughtRating(
|
||||||
|
thought_id=thought_objects[2].id,
|
||||||
|
user_id=user.id,
|
||||||
|
relevance_score=4
|
||||||
|
)
|
||||||
|
db.session.add_all([rating1, rating2])
|
||||||
|
|
||||||
|
# Kommentare erstellen
|
||||||
|
for thought in thought_objects:
|
||||||
|
comment = Comment(
|
||||||
|
content=f'Interessante Perspektive zu {thought.title}!',
|
||||||
|
thought_id=thought.id,
|
||||||
|
user_id=admin.id if thought.user_id != admin.id else user.id
|
||||||
|
)
|
||||||
|
db.session.add(comment)
|
||||||
|
|
||||||
|
# Benutzer-Mindmaps erstellen
|
||||||
|
user_mindmap = UserMindmap(
|
||||||
|
name='Meine KI-Forschung',
|
||||||
|
description='Meine persönliche Sammlung zu KI und Ethik',
|
||||||
|
user_id=user.id
|
||||||
|
)
|
||||||
|
db.session.add(user_mindmap)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Knoten zur Benutzer-Mindmap hinzufügen
|
||||||
|
user_mindmap_nodes = [
|
||||||
|
UserMindmapNode(
|
||||||
|
user_mindmap_id=user_mindmap.id,
|
||||||
|
node_id=nodes['machine_learning'].id,
|
||||||
|
x_position=200,
|
||||||
|
y_position=300
|
||||||
|
),
|
||||||
|
UserMindmapNode(
|
||||||
|
user_mindmap_id=user_mindmap.id,
|
||||||
|
node_id=nodes['ki_ethik'].id,
|
||||||
|
x_position=500,
|
||||||
|
y_position=200
|
||||||
|
)
|
||||||
|
]
|
||||||
|
db.session.add_all(user_mindmap_nodes)
|
||||||
|
|
||||||
|
# Private Notizen
|
||||||
|
note = MindmapNote(
|
||||||
|
user_id=user.id,
|
||||||
|
mindmap_id=user_mindmap.id,
|
||||||
|
node_id=nodes['ki_ethik'].id,
|
||||||
|
content="Recherchiere mehr über aktuelle ethische Richtlinien für KI-Entwicklung!",
|
||||||
|
color_code="#FFF59D"
|
||||||
|
)
|
||||||
|
db.session.add(note)
|
||||||
|
|
||||||
|
# Gedanken zu Bookmarks hinzufügen
|
||||||
|
user.bookmarked_thoughts.append(thought_objects[0])
|
||||||
|
admin.bookmarked_thoughts.append(thought_objects[1])
|
||||||
|
|
||||||
|
# Finaler Commit
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("Datenbank wurde erfolgreich initialisiert!")
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Alias für Kompatibilität mit älteren Scripts."""
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
init_database()
|
||||||
|
print("Datenbank wurde erfolgreich initialisiert!")
|
||||||
|
print("Sie können die Anwendung jetzt mit 'python app.py' starten")
|
||||||
|
print("Anmelden mit:")
|
||||||
|
print(" Admin: username=admin, password=admin")
|
||||||
|
print(" User: username=user, password=user")
|
||||||
230
models.py
Executable file
230
models.py
Executable file
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from datetime import datetime
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
# Beziehungstypen für Gedankenverknüpfungen
|
||||||
|
class RelationType(Enum):
|
||||||
|
SUPPORTS = "stützt"
|
||||||
|
CONTRADICTS = "widerspricht"
|
||||||
|
BUILDS_UPON = "baut auf auf"
|
||||||
|
GENERALIZES = "verallgemeinert"
|
||||||
|
SPECIFIES = "spezifiziert"
|
||||||
|
INSPIRES = "inspiriert"
|
||||||
|
|
||||||
|
# Beziehungstabelle für viele-zu-viele Beziehung zwischen MindMapNodes
|
||||||
|
node_relationship = db.Table('node_relationship',
|
||||||
|
db.Column('parent_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
|
||||||
|
db.Column('child_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehungstabelle für öffentliche Knoten und Gedanken
|
||||||
|
node_thought_association = db.Table('node_thought_association',
|
||||||
|
db.Column('node_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
|
||||||
|
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehungstabelle für Benutzer-spezifische Mindmap-Knoten und Gedanken
|
||||||
|
user_mindmap_thought_association = db.Table('user_mindmap_thought_association',
|
||||||
|
db.Column('user_mindmap_id', db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True),
|
||||||
|
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehungstabelle für Benutzer-Bookmarks von Gedanken
|
||||||
|
user_thought_bookmark = db.Table('user_thought_bookmark',
|
||||||
|
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
|
||||||
|
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True),
|
||||||
|
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
||||||
|
)
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(128))
|
||||||
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_login = db.Column(db.DateTime)
|
||||||
|
avatar = db.Column(db.String(200))
|
||||||
|
bio = db.Column(db.Text)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
||||||
|
comments = db.relationship('Comment', backref='author', lazy=True)
|
||||||
|
user_mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
|
||||||
|
mindmap_notes = db.relationship('MindmapNote', backref='user', lazy=True)
|
||||||
|
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
|
||||||
|
backref=db.backref('bookmarked_by', lazy='dynamic'))
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
class Category(db.Model):
|
||||||
|
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
color_code = db.Column(db.String(7)) # Hex color
|
||||||
|
icon = db.Column(db.String(50))
|
||||||
|
parent_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
children = db.relationship('Category', backref=db.backref('parent', remote_side=[id]))
|
||||||
|
nodes = db.relationship('MindMapNode', backref='category', lazy=True)
|
||||||
|
|
||||||
|
class MindMapNode(db.Model):
|
||||||
|
"""Öffentliche Mindmap-Knoten, die für alle Benutzer sichtbar sind"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(100), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
color_code = db.Column(db.String(7))
|
||||||
|
icon = db.Column(db.String(50))
|
||||||
|
is_public = db.Column(db.Boolean, default=True)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=True)
|
||||||
|
|
||||||
|
# Beziehungen für Baumstruktur (mehrere Eltern möglich)
|
||||||
|
parents = db.relationship(
|
||||||
|
'MindMapNode',
|
||||||
|
secondary=node_relationship,
|
||||||
|
primaryjoin=(node_relationship.c.child_id == id),
|
||||||
|
secondaryjoin=(node_relationship.c.parent_id == id),
|
||||||
|
backref=db.backref('children', lazy='dynamic'),
|
||||||
|
lazy='dynamic'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehungen zu Gedanken
|
||||||
|
thoughts = db.relationship('Thought',
|
||||||
|
secondary=node_thought_association,
|
||||||
|
backref=db.backref('nodes', lazy='dynamic'))
|
||||||
|
|
||||||
|
# Beziehung zum Ersteller
|
||||||
|
created_by = db.relationship('User', backref='created_nodes')
|
||||||
|
|
||||||
|
class UserMindmap(db.Model):
|
||||||
|
"""Benutzerspezifische Mindmap, die vom Benutzer personalisierbar ist"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(100), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
is_private = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
# Beziehungen zu öffentlichen Knoten
|
||||||
|
public_nodes = db.relationship('MindMapNode',
|
||||||
|
secondary='user_mindmap_node',
|
||||||
|
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
|
||||||
|
|
||||||
|
# Beziehungen zu Gedanken
|
||||||
|
thoughts = db.relationship('Thought',
|
||||||
|
secondary=user_mindmap_thought_association,
|
||||||
|
backref=db.backref('in_user_mindmaps', lazy='dynamic'))
|
||||||
|
|
||||||
|
# Notizen zu dieser Mindmap
|
||||||
|
notes = db.relationship('MindmapNote', backref='mindmap', lazy=True)
|
||||||
|
|
||||||
|
# Beziehungstabelle für benutzerorientierte Mindmaps und öffentliche Knoten
|
||||||
|
class UserMindmapNode(db.Model):
|
||||||
|
"""Speichert die Beziehung zwischen Benutzer-Mindmaps und öffentlichen Knoten inkl. Position"""
|
||||||
|
user_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), primary_key=True)
|
||||||
|
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True)
|
||||||
|
x_position = db.Column(db.Float, default=0) # Position X auf der Mindmap
|
||||||
|
y_position = db.Column(db.Float, default=0) # Position Y auf der Mindmap
|
||||||
|
scale = db.Column(db.Float, default=1.0) # Größe des Knotens
|
||||||
|
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
class MindmapNote(db.Model):
|
||||||
|
"""Private Notizen der Benutzer zu ihrer Mindmap"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
|
||||||
|
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
|
||||||
|
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
color_code = db.Column(db.String(7), default="#FFF59D") # Farbe der Notiz
|
||||||
|
|
||||||
|
class Thought(db.Model):
|
||||||
|
"""Gedanken und Inhalte, die in der Mindmap verknüpft werden können"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
title = db.Column(db.String(200), nullable=False)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
abstract = db.Column(db.Text)
|
||||||
|
keywords = db.Column(db.String(500))
|
||||||
|
color_code = db.Column(db.String(7)) # Hex color code
|
||||||
|
source_type = db.Column(db.String(50)) # PDF, Markdown, Text etc.
|
||||||
|
branch = db.Column(db.String(100), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
# Beziehungen
|
||||||
|
comments = db.relationship('Comment', backref='thought', lazy=True, cascade="all, delete-orphan")
|
||||||
|
ratings = db.relationship('ThoughtRating', backref='thought', lazy=True)
|
||||||
|
|
||||||
|
outgoing_relations = db.relationship(
|
||||||
|
'ThoughtRelation',
|
||||||
|
foreign_keys='ThoughtRelation.source_id',
|
||||||
|
backref='source_thought',
|
||||||
|
lazy=True
|
||||||
|
)
|
||||||
|
incoming_relations = db.relationship(
|
||||||
|
'ThoughtRelation',
|
||||||
|
foreign_keys='ThoughtRelation.target_id',
|
||||||
|
backref='target_thought',
|
||||||
|
lazy=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def average_rating(self):
|
||||||
|
if not self.ratings:
|
||||||
|
return 0
|
||||||
|
return sum(r.relevance_score for r in self.ratings) / len(self.ratings)
|
||||||
|
|
||||||
|
class ThoughtRelation(db.Model):
|
||||||
|
"""Beziehungen zwischen Gedanken"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
source_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||||
|
target_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||||
|
relation_type = db.Column(db.Enum(RelationType), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
|
||||||
|
# Beziehung zum Ersteller
|
||||||
|
created_by = db.relationship('User', backref='created_relations')
|
||||||
|
|
||||||
|
class ThoughtRating(db.Model):
|
||||||
|
"""Bewertungen von Gedanken durch Benutzer"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
relevance_score = db.Column(db.Integer, nullable=False) # 1-5
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.UniqueConstraint('thought_id', 'user_id', name='unique_thought_rating'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Beziehung zum Benutzer
|
||||||
|
user = db.relationship('User', backref='ratings')
|
||||||
|
|
||||||
|
class Comment(db.Model):
|
||||||
|
"""Kommentare zu Gedanken"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
content = db.Column(db.Text, nullable=False)
|
||||||
|
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
flask
|
flask==2.2.5
|
||||||
flask-login
|
flask-login==0.6.2
|
||||||
flask-wtf
|
flask-wtf
|
||||||
email-validator
|
email-validator
|
||||||
python-dotenv
|
python-dotenv
|
||||||
flask-sqlalchemy
|
werkzeug==2.2.3
|
||||||
|
flask-sqlalchemy==3.0.5
|
||||||
|
openai==1.3.0
|
||||||
|
requests==2.31.0
|
||||||
|
flask-cors==4.0.0
|
||||||
|
gunicorn==21.2.0
|
||||||
|
#pillow==10.0.1
|
||||||
|
pytest==7.4.0
|
||||||
|
pytest-flask==1.2.0
|
||||||
104
static/css/assistant.css
Normal file
104
static/css/assistant.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/* ChatGPT Assistent Styles */
|
||||||
|
#chatgpt-assistant {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-chat {
|
||||||
|
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-toggle {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-toggle:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-history {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-history::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-history::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assistant-history::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(156, 163, 175, 0.5);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #assistant-history::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(156, 163, 175, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mach Platz für Notifications, damit sie nicht mit dem Assistenten überlappen */
|
||||||
|
.notification-area {
|
||||||
|
bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserter Glassmorphism-Effekt */
|
||||||
|
.glass-morphism {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .glass-morphism {
|
||||||
|
background: rgba(15, 23, 42, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dunkleres Dark Theme */
|
||||||
|
.dark {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-dark-900 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgba(10, 15, 25, var(--tw-bg-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-dark-800 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgba(15, 23, 42, var(--tw-bg-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-dark-700 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgba(23, 33, 64, var(--tw-bg-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer immer unten */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
426
static/css/base-styles.css
Normal file
426
static/css/base-styles.css
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
/* Globale Variablen */
|
||||||
|
:root {
|
||||||
|
--dark-bg: #0e1220;
|
||||||
|
--dark-card-bg: rgba(24, 28, 45, 0.8);
|
||||||
|
--dark-element-bg: rgba(24, 28, 45, 0.8);
|
||||||
|
--light-bg: #f0f4f8;
|
||||||
|
--light-card-bg: rgba(255, 255, 255, 0.85);
|
||||||
|
--accent-color: #b38fff;
|
||||||
|
--accent-gradient: linear-gradient(135deg, #b38fff, #58a9ff);
|
||||||
|
--accent-gradient-hover: linear-gradient(135deg, #c7a8ff, #70b5ff);
|
||||||
|
--blur-amount: 20px;
|
||||||
|
--border-radius: 28px;
|
||||||
|
--card-border-radius: 24px;
|
||||||
|
--button-radius: 18px;
|
||||||
|
--nav-item-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Einstellungen */
|
||||||
|
html.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Styles */
|
||||||
|
html, body {
|
||||||
|
background-color: var(--dark-bg) !important;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
transition: background-color 0.5s ease, color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sicherstellen, dass der dunkle Hintergrund die gesamte Seite abdeckt */
|
||||||
|
#app-container, .container, main, .mx-auto, .py-12, #content-wrapper {
|
||||||
|
background-color: transparent !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Einstellungen */
|
||||||
|
html.light, html.light body {
|
||||||
|
background-color: var(--light-bg) !important;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Große Headings mit verbesserten Stilen */
|
||||||
|
h1.hero-heading {
|
||||||
|
font-size: clamp(2.5rem, 8vw, 5rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.section-heading {
|
||||||
|
font-size: clamp(1.75rem, 5vw, 3rem);
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Glasmorphismus-Stile */
|
||||||
|
.glass-morphism {
|
||||||
|
background: var(--dark-card-bg);
|
||||||
|
backdrop-filter: blur(var(--blur-amount));
|
||||||
|
-webkit-backdrop-filter: blur(var(--blur-amount));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: var(--card-border-radius);
|
||||||
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-morphism:hover {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-morphism-light {
|
||||||
|
background: var(--light-card-bg);
|
||||||
|
backdrop-filter: blur(var(--blur-amount));
|
||||||
|
-webkit-backdrop-filter: blur(var(--blur-amount));
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: var(--card-border-radius);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-morphism-light:hover {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Navbar-Styles */
|
||||||
|
.glass-navbar-dark {
|
||||||
|
background: rgba(14, 18, 32, 0.85);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||||
|
border-radius: 0 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-navbar-light {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border-color: rgba(0, 0, 0, 0.05);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 0 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Button-Stile mit besserer Lesbarkeit und stärkeren Farbverläufen */
|
||||||
|
.btn, button, .button, [type="button"], [type="submit"] {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
border-radius: var(--button-radius);
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(168, 85, 247, 0.8));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover, button:hover, .button:hover, [type="button"]:hover, [type="submit"]:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(129, 140, 248, 0.9), rgba(192, 132, 252, 0.9));
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.25), 0 0 12px rgba(179, 143, 255, 0.35);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active, button:active, .button:active, [type="button"]:active, [type="submit"]:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Stile mit verbesserten Farbverläufen */
|
||||||
|
.nav-link {
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
border-radius: var(--nav-item-radius);
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background: rgba(179, 143, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-active {
|
||||||
|
background: linear-gradient(135deg, rgba(124, 58, 237, 0.3), rgba(139, 92, 246, 0.3));
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 10%;
|
||||||
|
width: 80%;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light-Mode Navigation Stile */
|
||||||
|
.nav-link-light {
|
||||||
|
color: rgba(26, 32, 44, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light:hover {
|
||||||
|
background: rgba(179, 143, 255, 0.15);
|
||||||
|
color: rgba(26, 32, 44, 1);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light-active {
|
||||||
|
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(139, 92, 246, 0.2));
|
||||||
|
color: rgba(26, 32, 44, 1);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-light-active::after {
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(26, 32, 44, 0.5), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Entfernung von Gradient-Hintergrund überall */
|
||||||
|
.gradient-bg, .purple-gradient, .gradient-purple-bg {
|
||||||
|
background: var(--dark-bg) !important;
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Light-Mode-Stile für Buttons */
|
||||||
|
html.light .btn, html.light button, html.light .button,
|
||||||
|
html.light [type="button"], html.light [type="submit"] {
|
||||||
|
background: linear-gradient(135deg, rgba(124, 58, 237, 0.7), rgba(139, 92, 246, 0.7));
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .btn:hover, html.light button:hover, html.light .button:hover,
|
||||||
|
html.light [type="button"]:hover, html.light [type="submit"]:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(139, 92, 246, 0.85), rgba(168, 85, 247, 0.85));
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.12), 0 0 12px rgba(179, 143, 255, 0.2);
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Buttons mit Glasmorphismus */
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, rgba(179, 143, 255, 0.8), rgba(88, 169, 255, 0.8));
|
||||||
|
backdrop-filter: blur(var(--blur-amount));
|
||||||
|
-webkit-backdrop-filter: blur(var(--blur-amount));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: var(--button-radius);
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
|
||||||
|
background: linear-gradient(135deg, rgba(190, 160, 255, 0.9), rgba(100, 180, 255, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: rgba(32, 36, 55, 0.8);
|
||||||
|
backdrop-filter: blur(var(--blur-amount));
|
||||||
|
-webkit-backdrop-filter: blur(var(--blur-amount));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: var(--button-radius);
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
|
||||||
|
background: rgba(38, 42, 65, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Steuerungsbutton-Stil */
|
||||||
|
.control-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(32, 36, 55, 0.8);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background: rgba(38, 42, 65, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserter Farbverlauf-Text */
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(135deg, rgba(200, 170, 255, 1), rgba(100, 180, 255, 1));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
text-shadow: 0 2px 10px rgba(179, 143, 255, 0.3);
|
||||||
|
filter: drop-shadow(0 2px 6px rgba(179, 143, 255, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Globaler Hintergrund */
|
||||||
|
.full-page-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--dark-bg);
|
||||||
|
z-index: -10;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .full-page-bg {
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animationen für Hintergrundeffekte */
|
||||||
|
@keyframes float {
|
||||||
|
0% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-12px); }
|
||||||
|
100% { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { opacity: 0.7; transform: scale(1); }
|
||||||
|
50% { opacity: 1; transform: scale(1.05); }
|
||||||
|
100% { opacity: 0.7; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserter Container für konsistente Layouts */
|
||||||
|
.page-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Toggle Stile */
|
||||||
|
.dot {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 0.3s ease-in-out, background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ .dot {
|
||||||
|
transform: translateX(100%);
|
||||||
|
background-color: #58a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked ~ .block {
|
||||||
|
background-color: rgba(88, 169, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feature Cards mit Glasmorphismus und Farbverlauf */
|
||||||
|
.feature-card {
|
||||||
|
border-radius: var(--card-border-radius);
|
||||||
|
padding: 2rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: linear-gradient(145deg, rgba(32, 36, 55, 0.7), rgba(24, 28, 45, 0.9));
|
||||||
|
backdrop-filter: blur(var(--blur-amount));
|
||||||
|
-webkit-backdrop-filter: blur(var(--blur-amount));
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
background: linear-gradient(145deg, rgba(40, 44, 65, 0.8), rgba(28, 32, 50, 0.95));
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .feature-card {
|
||||||
|
background: linear-gradient(145deg, rgba(255, 255, 255, 0.8), rgba(240, 240, 250, 0.9));
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .feature-card:hover {
|
||||||
|
background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(245, 245, 255, 0.95));
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card .icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
background: linear-gradient(135deg, rgba(124, 58, 237, 0.8), rgba(139, 92, 246, 0.6));
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .feature-card h3 {
|
||||||
|
color: rgba(26, 32, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .feature-card p {
|
||||||
|
color: rgba(26, 32, 44, 0.75);
|
||||||
|
}
|
||||||
3884
static/css/main.css
Normal file
3884
static/css/main.css
Normal file
File diff suppressed because it is too large
Load Diff
125
static/css/src/cybernetwork-bg.css
Normal file
125
static/css/src/cybernetwork-bg.css
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/* Cybertechnisches Netzwerk Hintergrund-Overlay */
|
||||||
|
.cyber-network-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyber-network-bg::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(125deg,
|
||||||
|
rgba(14, 14, 22, 0.95) 0%,
|
||||||
|
rgba(30, 30, 46, 0.98) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-grid {
|
||||||
|
position: absolute;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
background-size: 40px 40px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(108, 93, 211, 0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(108, 93, 211, 0.05) 1px, transparent 1px);
|
||||||
|
transform: perspective(500px) rotateX(60deg);
|
||||||
|
animation: grid-move 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node {
|
||||||
|
position: absolute;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
background: rgba(76, 223, 255, 0.8);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 10px rgba(76, 223, 255, 0.6);
|
||||||
|
filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection {
|
||||||
|
position: absolute;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(76, 223, 255, 0.2) 0%,
|
||||||
|
rgba(108, 93, 211, 0.3) 50%,
|
||||||
|
rgba(76, 223, 255, 0.2) 100%);
|
||||||
|
transform-origin: left center;
|
||||||
|
animation: pulse 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-packet {
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(118, 69, 217, 0.8);
|
||||||
|
filter: blur(1px);
|
||||||
|
animation: travel var(--travel-time, 6s) linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 50% 40%,
|
||||||
|
rgba(76, 223, 255, 0.03) 0%,
|
||||||
|
rgba(108, 93, 211, 0.03) 45%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
opacity: 0.8;
|
||||||
|
animation: pulse-glow 8s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes grid-move {
|
||||||
|
0% {
|
||||||
|
transform: perspective(500px) rotateX(60deg) translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: perspective(500px) rotateX(60deg) translateY(40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes travel {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0) translateY(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(var(--travel-x, 100px)) translateY(var(--travel-y, 100px));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
207
static/css/src/input.css
Normal file
207
static/css/src/input.css
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
@apply scroll-smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-gray-50 text-gray-800 dark:bg-dark-800 dark:text-gray-100 font-sans;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
@apply font-bold text-gray-900 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-4xl md:text-5xl lg:text-6xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-3xl md:text-4xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-2xl md:text-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@apply text-xl md:text-2xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
@apply bg-white border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-dark-700 dark:border-dark-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder {
|
||||||
|
@apply text-gray-400 dark:text-gray-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center px-4 py-2 font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-offset-2 shadow-sm text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply btn bg-primary-600 hover:bg-primary-700 text-white focus:ring-primary-500 hover:shadow-md active:translate-y-0.5 dark:bg-primary-700 dark:hover:bg-primary-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply btn bg-secondary-600 hover:bg-secondary-700 text-white focus:ring-secondary-500 hover:shadow-md active:translate-y-0.5 dark:bg-secondary-700 dark:hover:bg-secondary-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
@apply btn border-2 border-gray-300 dark:border-dark-500 bg-white dark:bg-transparent text-gray-700 dark:text-gray-200 hover:bg-gray-50 hover:border-primary-500 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:border-primary-400 dark:hover:text-primary-400 focus:ring-gray-500 active:translate-y-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
@apply bg-white shadow-md dark:bg-dark-700 rounded-lg overflow-hidden border border-gray-200 dark:border-dark-600 hover:shadow-lg transition-shadow duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-text {
|
||||||
|
@apply text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-secondary-600 dark:from-primary-500 dark:to-secondary-500 font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-tooltip {
|
||||||
|
@apply max-w-xs p-3 bg-white text-gray-800 dark:bg-dark-800 dark:text-white rounded-lg shadow-lg text-sm z-50 border border-gray-200 dark:border-dark-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node {
|
||||||
|
@apply cursor-pointer transition-all duration-200 hover:shadow-lg border-2 border-gray-200 dark:border-dark-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mindmap-spezifische Stile */
|
||||||
|
.mindmap-container {
|
||||||
|
@apply bg-gray-50/80 dark:bg-dark-800/80 rounded-lg p-4 shadow-inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node-root {
|
||||||
|
@apply bg-primary-100 dark:bg-primary-900 text-primary-900 dark:text-primary-100 border-primary-300 dark:border-primary-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node-branch {
|
||||||
|
@apply bg-secondary-100 dark:bg-secondary-900 text-secondary-900 dark:text-secondary-100 border-secondary-300 dark:border-secondary-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-node-leaf {
|
||||||
|
@apply bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-gray-200 border-gray-300 dark:border-dark-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-link {
|
||||||
|
@apply stroke-gray-400 dark:stroke-gray-500 stroke-[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindmap-link-active {
|
||||||
|
@apply stroke-primary-500 dark:stroke-primary-400 stroke-[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ascii-art {
|
||||||
|
@apply font-mono text-xs leading-none whitespace-pre tracking-tight select-none text-primary-700 dark:text-primary-400 opacity-80 dark:opacity-60 font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Formulareingabefelder */
|
||||||
|
.form-input,
|
||||||
|
.form-textarea,
|
||||||
|
.form-select {
|
||||||
|
@apply w-full rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-800 shadow-sm
|
||||||
|
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
|
||||||
|
dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100 dark:focus:border-primary-400 dark:focus:ring-primary-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input-lg {
|
||||||
|
@apply py-3 text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input-sm {
|
||||||
|
@apply py-1 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox,
|
||||||
|
.form-radio {
|
||||||
|
@apply h-5 w-5 rounded border-gray-300 text-primary-600 shadow-sm
|
||||||
|
focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50
|
||||||
|
dark:border-dark-500 dark:bg-dark-700 dark:focus:border-primary-400 dark:focus:ring-primary-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-checkbox {
|
||||||
|
@apply rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-radio {
|
||||||
|
@apply rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
@apply mt-1 text-sm text-red-600 dark:text-red-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.tech-gradient {
|
||||||
|
@apply bg-gradient-to-r from-primary-600 to-secondary-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-effect {
|
||||||
|
@apply bg-white/95 backdrop-blur-md border border-gray-200 shadow-md dark:bg-dark-800/90 dark:border-dark-700/50 dark:shadow-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-ring {
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-primary-400 dark:focus:ring-offset-dark-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-focus {
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form validation styles */
|
||||||
|
.is-valid {
|
||||||
|
@apply border-green-500 dark:border-green-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-invalid {
|
||||||
|
@apply border-red-500 dark:border-red-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-valid:focus {
|
||||||
|
@apply ring-green-500/30 border-green-500 dark:ring-green-400/30 dark:border-green-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-invalid:focus {
|
||||||
|
@apply ring-red-500/30 border-red-500 dark:ring-red-400/30 dark:border-red-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
@apply text-xs text-gray-500 dark:text-gray-400 mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
|
@apply absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-500 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon {
|
||||||
|
@apply pl-10;
|
||||||
|
}
|
||||||
1449
static/css/style.css
Normal file
1449
static/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
456
static/d3-extensions.js
vendored
Normal file
456
static/d3-extensions.js
vendored
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
/**
|
||||||
|
* D3.js Erweiterungen für verbesserte Mindmap-Funktionalität
|
||||||
|
* Diese Datei enthält zusätzliche Hilfsfunktionen und Erweiterungen für D3.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
class D3Extensions {
|
||||||
|
/**
|
||||||
|
* Erstellt einen verbesserten radialen Farbverlauf
|
||||||
|
* @param {Object} defs - Das D3 defs Element
|
||||||
|
* @param {string} id - ID für den Gradienten
|
||||||
|
* @param {string} baseColor - Grundfarbe in hexadezimal oder RGB
|
||||||
|
* @returns {Object} - Das erstellte Gradient-Element
|
||||||
|
*/
|
||||||
|
static createEnhancedRadialGradient(defs, id, baseColor) {
|
||||||
|
// Farben berechnen
|
||||||
|
const d3Color = d3.color(baseColor);
|
||||||
|
const lightColor = d3Color.brighter(0.7);
|
||||||
|
const darkColor = d3Color.darker(0.3);
|
||||||
|
const midColor = d3Color;
|
||||||
|
|
||||||
|
// Gradient erstellen
|
||||||
|
const gradient = defs.append('radialGradient')
|
||||||
|
.attr('id', id)
|
||||||
|
.attr('cx', '30%')
|
||||||
|
.attr('cy', '30%')
|
||||||
|
.attr('r', '70%');
|
||||||
|
|
||||||
|
// Farbstops hinzufügen für realistischeren Verlauf
|
||||||
|
gradient.append('stop')
|
||||||
|
.attr('offset', '0%')
|
||||||
|
.attr('stop-color', lightColor.formatHex());
|
||||||
|
|
||||||
|
gradient.append('stop')
|
||||||
|
.attr('offset', '50%')
|
||||||
|
.attr('stop-color', midColor.formatHex());
|
||||||
|
|
||||||
|
gradient.append('stop')
|
||||||
|
.attr('offset', '100%')
|
||||||
|
.attr('stop-color', darkColor.formatHex());
|
||||||
|
|
||||||
|
return gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen Glüheffekt-Filter
|
||||||
|
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||||
|
* @param {String} id - ID des Filters
|
||||||
|
* @param {String} color - Farbe des Glüheffekts (Hex-Code)
|
||||||
|
* @param {Number} strength - Stärke des Glüheffekts
|
||||||
|
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||||
|
*/
|
||||||
|
static createGlowFilter(defs, id, color = '#b38fff', strength = 5) {
|
||||||
|
const filter = defs.append('filter')
|
||||||
|
.attr('id', id)
|
||||||
|
.attr('x', '-50%')
|
||||||
|
.attr('y', '-50%')
|
||||||
|
.attr('width', '200%')
|
||||||
|
.attr('height', '200%');
|
||||||
|
|
||||||
|
// Unschärfe-Effekt
|
||||||
|
filter.append('feGaussianBlur')
|
||||||
|
.attr('in', 'SourceGraphic')
|
||||||
|
.attr('stdDeviation', strength)
|
||||||
|
.attr('result', 'blur');
|
||||||
|
|
||||||
|
// Farbverstärkung für den Glüheffekt
|
||||||
|
filter.append('feColorMatrix')
|
||||||
|
.attr('in', 'blur')
|
||||||
|
.attr('type', 'matrix')
|
||||||
|
.attr('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -7')
|
||||||
|
.attr('result', 'glow');
|
||||||
|
|
||||||
|
// Farbflut mit der angegebenen Farbe
|
||||||
|
filter.append('feFlood')
|
||||||
|
.attr('flood-color', color)
|
||||||
|
.attr('flood-opacity', '0.7')
|
||||||
|
.attr('result', 'color');
|
||||||
|
|
||||||
|
// Zusammensetzen des Glüheffekts mit der Farbe
|
||||||
|
filter.append('feComposite')
|
||||||
|
.attr('in', 'color')
|
||||||
|
.attr('in2', 'glow')
|
||||||
|
.attr('operator', 'in')
|
||||||
|
.attr('result', 'glow-color');
|
||||||
|
|
||||||
|
// Zusammenfügen aller Ebenen
|
||||||
|
const feMerge = filter.append('feMerge');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'glow-color');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'SourceGraphic');
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet eine konsistente Farbe aus einem String
|
||||||
|
* @param {string} str - Eingabestring
|
||||||
|
* @returns {string} - Generierte Farbe als Hex-String
|
||||||
|
*/
|
||||||
|
static stringToColor(str) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basis-Farbpalette für konsistente Farben
|
||||||
|
const colorPalette = [
|
||||||
|
"#4299E1", // Blau
|
||||||
|
"#9F7AEA", // Lila
|
||||||
|
"#ED64A6", // Pink
|
||||||
|
"#48BB78", // Grün
|
||||||
|
"#ECC94B", // Gelb
|
||||||
|
"#F56565", // Rot
|
||||||
|
"#38B2AC", // Türkis
|
||||||
|
"#ED8936", // Orange
|
||||||
|
"#667EEA", // Indigo
|
||||||
|
];
|
||||||
|
|
||||||
|
// Farbe aus der Palette wählen basierend auf dem Hash
|
||||||
|
const colorIndex = Math.abs(hash) % colorPalette.length;
|
||||||
|
return colorPalette[colorIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen Schatteneffekt-Filter
|
||||||
|
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||||
|
* @param {String} id - ID des Filters
|
||||||
|
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||||
|
*/
|
||||||
|
static createShadowFilter(defs, id) {
|
||||||
|
const filter = defs.append('filter')
|
||||||
|
.attr('id', id)
|
||||||
|
.attr('x', '-50%')
|
||||||
|
.attr('y', '-50%')
|
||||||
|
.attr('width', '200%')
|
||||||
|
.attr('height', '200%');
|
||||||
|
|
||||||
|
// Einfacher Schlagschatten
|
||||||
|
filter.append('feDropShadow')
|
||||||
|
.attr('dx', 0)
|
||||||
|
.attr('dy', 4)
|
||||||
|
.attr('stdDeviation', 4)
|
||||||
|
.attr('flood-color', 'rgba(0, 0, 0, 0.3)');
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen Glasmorphismus-Effekt-Filter
|
||||||
|
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||||
|
* @param {String} id - ID des Filters
|
||||||
|
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||||
|
*/
|
||||||
|
static createGlassMorphismFilter(defs, id) {
|
||||||
|
const filter = defs.append('filter')
|
||||||
|
.attr('id', id)
|
||||||
|
.attr('x', '-50%')
|
||||||
|
.attr('y', '-50%')
|
||||||
|
.attr('width', '200%')
|
||||||
|
.attr('height', '200%');
|
||||||
|
|
||||||
|
// Hintergrund-Unschärfe für den Glaseffekt
|
||||||
|
filter.append('feGaussianBlur')
|
||||||
|
.attr('in', 'SourceGraphic')
|
||||||
|
.attr('stdDeviation', 8)
|
||||||
|
.attr('result', 'blur');
|
||||||
|
|
||||||
|
// Hellere Farbe für den Glaseffekt
|
||||||
|
filter.append('feColorMatrix')
|
||||||
|
.attr('in', 'blur')
|
||||||
|
.attr('type', 'matrix')
|
||||||
|
.attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.1 0 0 0 0.6 0')
|
||||||
|
.attr('result', 'glass');
|
||||||
|
|
||||||
|
// Überlagerung mit dem Original
|
||||||
|
const feMerge = filter.append('feMerge');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'glass');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'SourceGraphic');
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen verstärkten Glasmorphismus-Effekt mit Farbverlauf
|
||||||
|
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||||
|
* @param {String} id - ID des Filters
|
||||||
|
* @param {String} color1 - Erste Farbe des Verlaufs (Hex-Code)
|
||||||
|
* @param {String} color2 - Zweite Farbe des Verlaufs (Hex-Code)
|
||||||
|
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||||
|
*/
|
||||||
|
static createEnhancedGlassMorphismFilter(defs, id, color1 = '#b38fff', color2 = '#58a9ff') {
|
||||||
|
// Farbverlauf für den Glaseffekt definieren
|
||||||
|
const gradientId = `gradient-${id}`;
|
||||||
|
const gradient = defs.append('linearGradient')
|
||||||
|
.attr('id', gradientId)
|
||||||
|
.attr('x1', '0%')
|
||||||
|
.attr('y1', '0%')
|
||||||
|
.attr('x2', '100%')
|
||||||
|
.attr('y2', '100%');
|
||||||
|
|
||||||
|
gradient.append('stop')
|
||||||
|
.attr('offset', '0%')
|
||||||
|
.attr('stop-color', color1)
|
||||||
|
.attr('stop-opacity', '0.3');
|
||||||
|
|
||||||
|
gradient.append('stop')
|
||||||
|
.attr('offset', '100%')
|
||||||
|
.attr('stop-color', color2)
|
||||||
|
.attr('stop-opacity', '0.3');
|
||||||
|
|
||||||
|
// Filter erstellen
|
||||||
|
const filter = defs.append('filter')
|
||||||
|
.attr('id', id)
|
||||||
|
.attr('x', '-50%')
|
||||||
|
.attr('y', '-50%')
|
||||||
|
.attr('width', '200%')
|
||||||
|
.attr('height', '200%');
|
||||||
|
|
||||||
|
// Hintergrund-Unschärfe
|
||||||
|
filter.append('feGaussianBlur')
|
||||||
|
.attr('in', 'SourceGraphic')
|
||||||
|
.attr('stdDeviation', 6)
|
||||||
|
.attr('result', 'blur');
|
||||||
|
|
||||||
|
// Farbverlauf einfügen
|
||||||
|
const feImage = filter.append('feImage')
|
||||||
|
.attr('xlink:href', `#${gradientId}`)
|
||||||
|
.attr('result', 'gradient')
|
||||||
|
.attr('x', '0%')
|
||||||
|
.attr('y', '0%')
|
||||||
|
.attr('width', '100%')
|
||||||
|
.attr('height', '100%')
|
||||||
|
.attr('preserveAspectRatio', 'none');
|
||||||
|
|
||||||
|
// Zusammenfügen aller Ebenen
|
||||||
|
const feMerge = filter.append('feMerge');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'blur');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'gradient');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'SourceGraphic');
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen 3D-Glaseffekt mit verbesserter Tiefe und Reflexionen
|
||||||
|
* @param {Object} defs - D3-Referenz auf den defs-Bereich
|
||||||
|
* @param {String} id - ID des Filters
|
||||||
|
* @returns {Object} D3-Referenz auf den erstellten Filter
|
||||||
|
*/
|
||||||
|
static create3DGlassEffect(defs, id) {
|
||||||
|
const filter = defs.append('filter')
|
||||||
|
.attr('id', id)
|
||||||
|
.attr('x', '-50%')
|
||||||
|
.attr('y', '-50%')
|
||||||
|
.attr('width', '200%')
|
||||||
|
.attr('height', '200%');
|
||||||
|
|
||||||
|
// Farbmatrix für Transparenz
|
||||||
|
filter.append('feColorMatrix')
|
||||||
|
.attr('type', 'matrix')
|
||||||
|
.attr('in', 'SourceGraphic')
|
||||||
|
.attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0')
|
||||||
|
.attr('result', 'transparent');
|
||||||
|
|
||||||
|
// Hintergrund-Unschärfe für Tiefe
|
||||||
|
filter.append('feGaussianBlur')
|
||||||
|
.attr('in', 'transparent')
|
||||||
|
.attr('stdDeviation', '4')
|
||||||
|
.attr('result', 'blurred');
|
||||||
|
|
||||||
|
// Lichtquelle und Schattierung hinzufügen
|
||||||
|
const lightSource = filter.append('feSpecularLighting')
|
||||||
|
.attr('in', 'blurred')
|
||||||
|
.attr('surfaceScale', '6')
|
||||||
|
.attr('specularConstant', '1')
|
||||||
|
.attr('specularExponent', '30')
|
||||||
|
.attr('lighting-color', '#ffffff')
|
||||||
|
.attr('result', 'specular');
|
||||||
|
|
||||||
|
lightSource.append('fePointLight')
|
||||||
|
.attr('x', '100')
|
||||||
|
.attr('y', '100')
|
||||||
|
.attr('z', '200');
|
||||||
|
|
||||||
|
// Lichtreflexion verstärken
|
||||||
|
filter.append('feComposite')
|
||||||
|
.attr('in', 'specular')
|
||||||
|
.attr('in2', 'SourceGraphic')
|
||||||
|
.attr('operator', 'in')
|
||||||
|
.attr('result', 'specularHighlight');
|
||||||
|
|
||||||
|
// Inneren Schatten erzeugen
|
||||||
|
const innerShadow = filter.append('feOffset')
|
||||||
|
.attr('in', 'SourceAlpha')
|
||||||
|
.attr('dx', '0')
|
||||||
|
.attr('dy', '1')
|
||||||
|
.attr('result', 'offsetblur');
|
||||||
|
|
||||||
|
innerShadow.append('feGaussianBlur')
|
||||||
|
.attr('in', 'offsetblur')
|
||||||
|
.attr('stdDeviation', '2')
|
||||||
|
.attr('result', 'innerShadow');
|
||||||
|
|
||||||
|
filter.append('feComposite')
|
||||||
|
.attr('in', 'innerShadow')
|
||||||
|
.attr('in2', 'SourceGraphic')
|
||||||
|
.attr('operator', 'out')
|
||||||
|
.attr('result', 'innerShadowEffect');
|
||||||
|
|
||||||
|
// Schichten kombinieren
|
||||||
|
const feMerge = filter.append('feMerge');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'blurred');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'innerShadowEffect');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'specularHighlight');
|
||||||
|
feMerge.append('feMergeNode')
|
||||||
|
.attr('in', 'SourceGraphic');
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt einen Partikelsystem-Effekt für interaktive Knoten hinzu
|
||||||
|
* @param {Object} parent - Das übergeordnete SVG-Element
|
||||||
|
* @param {number} x - X-Koordinate des Zentrums
|
||||||
|
* @param {number} y - Y-Koordinate des Zentrums
|
||||||
|
* @param {string} color - Partikelfarbe (Hex-Code)
|
||||||
|
* @param {number} count - Anzahl der Partikel
|
||||||
|
*/
|
||||||
|
static createParticleEffect(parent, x, y, color = '#b38fff', count = 5) {
|
||||||
|
const particles = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const particle = parent.append('circle')
|
||||||
|
.attr('cx', x)
|
||||||
|
.attr('cy', y)
|
||||||
|
.attr('r', 0)
|
||||||
|
.attr('fill', color)
|
||||||
|
.style('opacity', 0.8);
|
||||||
|
|
||||||
|
particles.push(particle);
|
||||||
|
|
||||||
|
// Partikel animieren
|
||||||
|
animateParticle(particle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateParticle(particle) {
|
||||||
|
// Zufällige Richtung und Geschwindigkeit
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const speed = 1 + Math.random() * 2;
|
||||||
|
const distance = 20 + Math.random() * 30;
|
||||||
|
|
||||||
|
// Zielposition berechnen
|
||||||
|
const targetX = x + Math.cos(angle) * distance;
|
||||||
|
const targetY = y + Math.sin(angle) * distance;
|
||||||
|
|
||||||
|
// Animation mit zufälliger Dauer
|
||||||
|
const duration = 1000 + Math.random() * 500;
|
||||||
|
|
||||||
|
particle
|
||||||
|
.attr('r', 0)
|
||||||
|
.style('opacity', 0.8)
|
||||||
|
.transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr('cx', targetX)
|
||||||
|
.attr('cy', targetY)
|
||||||
|
.attr('r', 2 + Math.random() * 3)
|
||||||
|
.style('opacity', 0)
|
||||||
|
.on('end', function() {
|
||||||
|
// Partikel entfernen
|
||||||
|
particle.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Führt eine Pulsanimation auf einem Knoten durch
|
||||||
|
* @param {Object} node - D3-Knoten-Selektion
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
static pulseAnimation(node) {
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const circle = node.select('circle');
|
||||||
|
const originalRadius = parseFloat(circle.attr('r'));
|
||||||
|
const originalFill = circle.attr('fill');
|
||||||
|
|
||||||
|
// Pulsanimation
|
||||||
|
circle
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr('r', originalRadius * 1.3)
|
||||||
|
.attr('fill', '#b38fff')
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr('r', originalRadius)
|
||||||
|
.attr('fill', originalFill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet eine adaptive Schriftgröße basierend auf der Textlänge
|
||||||
|
* @param {string} text - Der anzuzeigende Text
|
||||||
|
* @param {number} maxSize - Maximale Schriftgröße in Pixel
|
||||||
|
* @param {number} minSize - Minimale Schriftgröße in Pixel
|
||||||
|
* @returns {number} - Die berechnete Schriftgröße
|
||||||
|
*/
|
||||||
|
static getAdaptiveFontSize(text, maxSize = 14, minSize = 10) {
|
||||||
|
if (!text) return maxSize;
|
||||||
|
|
||||||
|
// Linear die Schriftgröße basierend auf der Textlänge anpassen
|
||||||
|
const length = text.length;
|
||||||
|
if (length <= 6) return maxSize;
|
||||||
|
if (length >= 20) return minSize;
|
||||||
|
|
||||||
|
// Lineare Interpolation
|
||||||
|
const factor = (length - 6) / (20 - 6);
|
||||||
|
return maxSize - factor * (maxSize - minSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt einen Pulsierenden Effekt zu einer Selektion hinzu
|
||||||
|
* @param {Object} selection - D3-Selektion
|
||||||
|
* @param {number} duration - Dauer eines Puls-Zyklus in ms
|
||||||
|
* @param {number} minOpacity - Minimale Opazität
|
||||||
|
* @param {number} maxOpacity - Maximale Opazität
|
||||||
|
*/
|
||||||
|
static addPulseEffect(selection, duration = 1500, minOpacity = 0.4, maxOpacity = 0.9) {
|
||||||
|
function pulse() {
|
||||||
|
selection
|
||||||
|
.transition()
|
||||||
|
.duration(duration / 2)
|
||||||
|
.style('opacity', minOpacity)
|
||||||
|
.transition()
|
||||||
|
.duration(duration / 2)
|
||||||
|
.style('opacity', maxOpacity)
|
||||||
|
.on('end', pulse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialen Stil setzen
|
||||||
|
selection.style('opacity', maxOpacity);
|
||||||
|
|
||||||
|
// Pulsanimation starten
|
||||||
|
pulse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Globale Verfügbarkeit sicherstellen
|
||||||
|
window.D3Extensions = D3Extensions;
|
||||||
25
static/img/favicon-gen.py
Normal file
25
static/img/favicon-gen.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
from PIL import Image
|
||||||
|
import cairosvg
|
||||||
|
|
||||||
|
# Pfad zum SVG-Favicon
|
||||||
|
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
||||||
|
# Ausgabepfad für das PNG
|
||||||
|
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
||||||
|
# Ausgabepfad für das ICO
|
||||||
|
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
||||||
|
|
||||||
|
# SVG zu PNG konvertieren
|
||||||
|
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
||||||
|
|
||||||
|
# PNG zu ICO konvertieren
|
||||||
|
img = Image.open(png_path)
|
||||||
|
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
||||||
|
|
||||||
|
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
||||||
|
|
||||||
|
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
||||||
|
# os.remove(png_path)
|
||||||
21
static/img/favicon.svg
Normal file
21
static/img/favicon.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="512" height="512" rx="128" fill="url(#paint0_linear)" />
|
||||||
|
<path d="M143.5 384V128H180.5L256.5 270L332.5 128H369.5V384H328.5V196L256.5 332H256L183.5 196V384H143.5Z" fill="white"/>
|
||||||
|
<circle cx="143.5" cy="128" r="20" fill="#a040ff" />
|
||||||
|
<circle cx="256.5" cy="270" r="25" fill="#a040ff" />
|
||||||
|
<circle cx="369.5" cy="128" r="20" fill="#a040ff" />
|
||||||
|
<circle cx="143.5" cy="384" r="20" fill="#4080ff" />
|
||||||
|
<circle cx="369.5" cy="384" r="20" fill="#4080ff" />
|
||||||
|
<path d="M143.5 128L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||||
|
<path d="M256.5 270L183.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||||
|
<path d="M256.5 270L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||||
|
<path d="M369.5 128L328.5 196" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||||
|
<path d="M183.5 196L143.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||||
|
<path d="M328.5 196L369.5 384" stroke="white" stroke-width="4" stroke-linecap="round" />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#205cf5" />
|
||||||
|
<stop offset="1" stop-color="#8020f5" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
229
static/js/main.js
Normal file
229
static/js/main.js
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* MindMap - Hauptdatei für globale JavaScript-Funktionen
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import des ChatGPT-Assistenten
|
||||||
|
import ChatGPTAssistant from './modules/chatgpt-assistant.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hauptmodul für die MindMap-Anwendung
|
||||||
|
* Verwaltet die globale Anwendungslogik
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialisiere die Anwendung
|
||||||
|
MindMap.init();
|
||||||
|
|
||||||
|
// Wende Dunkel-/Hellmodus an
|
||||||
|
const isDarkMode = localStorage.getItem('darkMode') === 'dark';
|
||||||
|
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hauptobjekt der MindMap-Anwendung
|
||||||
|
*/
|
||||||
|
const MindMap = {
|
||||||
|
// App-Status
|
||||||
|
initialized: false,
|
||||||
|
darkMode: document.documentElement.classList.contains('dark'),
|
||||||
|
pageInitializers: {},
|
||||||
|
currentPage: document.body.dataset.page,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert die MindMap-Anwendung
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
console.log('MindMap-Anwendung wird initialisiert...');
|
||||||
|
|
||||||
|
// Initialisiere den ChatGPT-Assistenten
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.init();
|
||||||
|
// Speichere als Teil von MindMap
|
||||||
|
this.assistant = assistant;
|
||||||
|
|
||||||
|
// Seiten-spezifische Initialisierer aufrufen
|
||||||
|
if (this.currentPage && this.pageInitializers[this.currentPage]) {
|
||||||
|
this.pageInitializers[this.currentPage]();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener einrichten
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Dunkel-/Hellmodus aus LocalStorage wiederherstellen
|
||||||
|
if (localStorage.getItem('darkMode') === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
this.darkMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mindmap initialisieren, falls auf der richtigen Seite
|
||||||
|
this.initializeMindmap();
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert die D3.js Mindmap-Visualisierung
|
||||||
|
*/
|
||||||
|
initializeMindmap() {
|
||||||
|
// Prüfe, ob wir auf der Mindmap-Seite sind
|
||||||
|
const mindmapContainer = document.getElementById('mindmap-container');
|
||||||
|
if (!mindmapContainer) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Initialisiere Mindmap...');
|
||||||
|
|
||||||
|
// Initialisiere die Mindmap
|
||||||
|
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||||
|
height: mindmapContainer.clientHeight || 600,
|
||||||
|
nodeRadius: 18,
|
||||||
|
selectedNodeRadius: 24,
|
||||||
|
linkDistance: 150,
|
||||||
|
onNodeClick: this.handleNodeClick.bind(this)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Globale Referenz für andere Module
|
||||||
|
window.mindmapInstance = mindmap;
|
||||||
|
|
||||||
|
// Event-Listener für Zoom-Buttons
|
||||||
|
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||||
|
if (zoomInBtn) {
|
||||||
|
zoomInBtn.addEventListener('click', () => {
|
||||||
|
const svg = d3.select('#mindmap-container svg');
|
||||||
|
const currentZoom = d3.zoomTransform(svg.node());
|
||||||
|
const newScale = currentZoom.k * 1.3;
|
||||||
|
svg.transition().duration(300).call(
|
||||||
|
d3.zoom().transform,
|
||||||
|
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||||
|
if (zoomOutBtn) {
|
||||||
|
zoomOutBtn.addEventListener('click', () => {
|
||||||
|
const svg = d3.select('#mindmap-container svg');
|
||||||
|
const currentZoom = d3.zoomTransform(svg.node());
|
||||||
|
const newScale = currentZoom.k / 1.3;
|
||||||
|
svg.transition().duration(300).call(
|
||||||
|
d3.zoom().transform,
|
||||||
|
d3.zoomIdentity.translate(currentZoom.x, currentZoom.y).scale(newScale)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerBtn = document.getElementById('center-btn');
|
||||||
|
if (centerBtn) {
|
||||||
|
centerBtn.addEventListener('click', () => {
|
||||||
|
const svg = d3.select('#mindmap-container svg');
|
||||||
|
svg.transition().duration(500).call(
|
||||||
|
d3.zoom().transform,
|
||||||
|
d3.zoomIdentity.scale(1)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Add-Thought-Button
|
||||||
|
const addThoughtBtn = document.getElementById('add-thought-btn');
|
||||||
|
if (addThoughtBtn) {
|
||||||
|
addThoughtBtn.addEventListener('click', () => {
|
||||||
|
this.showAddThoughtDialog();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für Connect-Button
|
||||||
|
const connectBtn = document.getElementById('connect-btn');
|
||||||
|
if (connectBtn) {
|
||||||
|
connectBtn.addEventListener('click', () => {
|
||||||
|
this.showConnectDialog();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der Initialisierung der Mindmap:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler für Klick auf einen Knoten in der Mindmap
|
||||||
|
* @param {Object} node - Der angeklickte Knoten
|
||||||
|
*/
|
||||||
|
handleNodeClick(node) {
|
||||||
|
console.log('Knoten wurde angeklickt:', node);
|
||||||
|
|
||||||
|
// Hier könnte man Logik hinzufügen, um Detailinformationen anzuzeigen
|
||||||
|
// oder den ausgewählten Knoten hervorzuheben
|
||||||
|
const detailsContainer = document.getElementById('node-details');
|
||||||
|
if (detailsContainer) {
|
||||||
|
detailsContainer.innerHTML = `
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="text-xl font-bold mb-2">${node.name}</h3>
|
||||||
|
<p class="text-gray-300 mb-4">${node.description || 'Keine Beschreibung verfügbar.'}</p>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm">
|
||||||
|
<i class="fas fa-brain mr-1"></i> ${node.thought_count || 0} Gedanken
|
||||||
|
</span>
|
||||||
|
<button class="px-3 py-1 bg-purple-600 bg-opacity-30 rounded-lg text-sm">
|
||||||
|
<i class="fas fa-plus mr-1"></i> Gedanke hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Button zum Hinzufügen eines Gedankens
|
||||||
|
const addThoughtBtn = detailsContainer.querySelector('button');
|
||||||
|
addThoughtBtn.addEventListener('click', () => {
|
||||||
|
this.showAddThoughtDialog(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog zum Hinzufügen eines neuen Knotens
|
||||||
|
*/
|
||||||
|
showAddNodeDialog() {
|
||||||
|
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
|
||||||
|
alert('Diese Funktion steht bald zur Verfügung!');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog zum Hinzufügen eines neuen Gedankens zu einem Knoten
|
||||||
|
*/
|
||||||
|
showAddThoughtDialog(node) {
|
||||||
|
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
|
||||||
|
alert('Diese Funktion steht bald zur Verfügung!');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog zum Verbinden von Knoten
|
||||||
|
*/
|
||||||
|
showConnectDialog() {
|
||||||
|
// Diese Funktionalität würde in einer vollständigen Implementierung eingebunden werden
|
||||||
|
alert('Diese Funktion steht bald zur Verfügung!');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Richtet Event-Listener für die Benutzeroberfläche ein
|
||||||
|
*/
|
||||||
|
setupEventListeners() {
|
||||||
|
// Event-Listener für Dark Mode-Wechsel
|
||||||
|
document.addEventListener('darkModeToggled', (event) => {
|
||||||
|
this.darkMode = event.detail.isDark;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Responsive Anpassungen bei Fenstergröße
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (window.mindmapInstance) {
|
||||||
|
const container = document.getElementById('mindmap-container');
|
||||||
|
if (container) {
|
||||||
|
window.mindmapInstance.width = container.clientWidth;
|
||||||
|
window.mindmapInstance.height = container.clientHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Globale Export für andere Module
|
||||||
|
window.MindMap = MindMap;
|
||||||
280
static/js/modules/chatgpt-assistant.js
Normal file
280
static/js/modules/chatgpt-assistant.js
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* ChatGPT Assistent Modul
|
||||||
|
* Verwaltet die Interaktion mit der OpenAI API und die Benutzeroberfläche des Assistenten
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ChatGPTAssistant {
|
||||||
|
constructor() {
|
||||||
|
this.messages = [];
|
||||||
|
this.isOpen = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
this.container = null;
|
||||||
|
this.chatHistory = null;
|
||||||
|
this.inputField = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert den Assistenten und fügt die UI zum DOM hinzu
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// Assistent-Container erstellen
|
||||||
|
this.createAssistantUI();
|
||||||
|
|
||||||
|
// Event-Listener hinzufügen
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Ersten Willkommensnachricht anzeigen
|
||||||
|
this.addMessage("assistant", "Frage den KI-Assistenten");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt die UI-Elemente für den Assistenten
|
||||||
|
*/
|
||||||
|
createAssistantUI() {
|
||||||
|
// Hauptcontainer erstellen
|
||||||
|
this.container = document.createElement('div');
|
||||||
|
this.container.id = 'chatgpt-assistant';
|
||||||
|
this.container.className = 'fixed bottom-4 right-4 z-50 flex flex-col';
|
||||||
|
|
||||||
|
// Button zum Öffnen/Schließen des Assistenten
|
||||||
|
const toggleButton = document.createElement('button');
|
||||||
|
toggleButton.id = 'assistant-toggle';
|
||||||
|
toggleButton.className = 'ml-auto bg-primary-600 hover:bg-primary-700 text-white rounded-full p-3 shadow-lg transition-all duration-300 mb-2';
|
||||||
|
toggleButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
|
||||||
|
|
||||||
|
// Chat-Container
|
||||||
|
const chatContainer = document.createElement('div');
|
||||||
|
chatContainer.id = 'assistant-chat';
|
||||||
|
chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 sm:w-96 max-h-0 opacity-0';
|
||||||
|
|
||||||
|
// Chat-Header
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'bg-primary-600 text-white p-3 flex items-center justify-between';
|
||||||
|
header.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-robot mr-2"></i>
|
||||||
|
<span>KI-Assistent</span>
|
||||||
|
</div>
|
||||||
|
<button id="assistant-close" class="text-white hover:text-gray-200">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Chat-Verlauf
|
||||||
|
this.chatHistory = document.createElement('div');
|
||||||
|
this.chatHistory.id = 'assistant-history';
|
||||||
|
this.chatHistory.className = 'p-3 overflow-y-auto max-h-80 space-y-3';
|
||||||
|
|
||||||
|
// Chat-Eingabe
|
||||||
|
const inputContainer = document.createElement('div');
|
||||||
|
inputContainer.className = 'border-t border-gray-200 dark:border-dark-600 p-3 flex items-center';
|
||||||
|
|
||||||
|
this.inputField = document.createElement('input');
|
||||||
|
this.inputField.type = 'text';
|
||||||
|
this.inputField.placeholder = 'Frage den KI-Assistenten';
|
||||||
|
this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500';
|
||||||
|
|
||||||
|
const sendButton = document.createElement('button');
|
||||||
|
sendButton.id = 'assistant-send';
|
||||||
|
sendButton.className = 'bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-r-lg';
|
||||||
|
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
||||||
|
|
||||||
|
// Elemente zusammenfügen
|
||||||
|
inputContainer.appendChild(this.inputField);
|
||||||
|
inputContainer.appendChild(sendButton);
|
||||||
|
|
||||||
|
chatContainer.appendChild(header);
|
||||||
|
chatContainer.appendChild(this.chatHistory);
|
||||||
|
chatContainer.appendChild(inputContainer);
|
||||||
|
|
||||||
|
this.container.appendChild(toggleButton);
|
||||||
|
this.container.appendChild(chatContainer);
|
||||||
|
|
||||||
|
// Zum DOM hinzufügen
|
||||||
|
document.body.appendChild(this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Richtet Event-Listener für die Benutzeroberfläche ein
|
||||||
|
*/
|
||||||
|
setupEventListeners() {
|
||||||
|
// Toggle-Button
|
||||||
|
const toggleButton = document.getElementById('assistant-toggle');
|
||||||
|
toggleButton.addEventListener('click', () => this.toggleAssistant());
|
||||||
|
|
||||||
|
// Schließen-Button
|
||||||
|
const closeButton = document.getElementById('assistant-close');
|
||||||
|
closeButton.addEventListener('click', () => this.toggleAssistant(false));
|
||||||
|
|
||||||
|
// Senden-Button
|
||||||
|
const sendButton = document.getElementById('assistant-send');
|
||||||
|
sendButton.addEventListener('click', () => this.sendMessage());
|
||||||
|
|
||||||
|
// Enter-Taste im Eingabefeld
|
||||||
|
this.inputField.addEventListener('keyup', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet oder schließt den Assistenten
|
||||||
|
* @param {boolean} state - Optional: erzwingt einen bestimmten Zustand
|
||||||
|
*/
|
||||||
|
toggleAssistant(state = null) {
|
||||||
|
const chatContainer = document.getElementById('assistant-chat');
|
||||||
|
this.isOpen = state !== null ? state : !this.isOpen;
|
||||||
|
|
||||||
|
if (this.isOpen) {
|
||||||
|
chatContainer.classList.remove('max-h-0', 'opacity-0');
|
||||||
|
chatContainer.classList.add('max-h-96', 'opacity-100');
|
||||||
|
this.inputField.focus();
|
||||||
|
} else {
|
||||||
|
chatContainer.classList.remove('max-h-96', 'opacity-100');
|
||||||
|
chatContainer.classList.add('max-h-0', 'opacity-0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt eine Nachricht zum Chat-Verlauf hinzu
|
||||||
|
* @param {string} sender - 'user' oder 'assistant'
|
||||||
|
* @param {string} text - Nachrichtentext
|
||||||
|
*/
|
||||||
|
addMessage(sender, text) {
|
||||||
|
// Nachricht zum Verlauf hinzufügen
|
||||||
|
this.messages.push({ role: sender, content: text });
|
||||||
|
|
||||||
|
// DOM-Element erstellen
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.className = `flex ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
|
||||||
|
|
||||||
|
const bubble = document.createElement('div');
|
||||||
|
bubble.className = sender === 'user'
|
||||||
|
? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'
|
||||||
|
: 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
|
bubble.textContent = text;
|
||||||
|
|
||||||
|
messageEl.appendChild(bubble);
|
||||||
|
this.chatHistory.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Scroll zum Ende des Verlaufs
|
||||||
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet die Benutzernachricht an den Server und zeigt die Antwort an
|
||||||
|
*/
|
||||||
|
async sendMessage() {
|
||||||
|
const userInput = this.inputField.value.trim();
|
||||||
|
if (!userInput || this.isLoading) return;
|
||||||
|
|
||||||
|
// Benutzernachricht anzeigen
|
||||||
|
this.addMessage('user', userInput);
|
||||||
|
|
||||||
|
// Eingabefeld zurücksetzen
|
||||||
|
this.inputField.value = '';
|
||||||
|
|
||||||
|
// Ladeindikator anzeigen
|
||||||
|
this.isLoading = true;
|
||||||
|
this.showLoadingIndicator();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Anfrage an den Server senden
|
||||||
|
const response = await fetch('/api/assistant', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages: this.messages
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Netzwerkfehler oder Serverproblem');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Ladeindikator entfernen
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
|
// Antwort anzeigen
|
||||||
|
this.addMessage('assistant', data.response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||||
|
|
||||||
|
// Ladeindikator entfernen
|
||||||
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
|
// Fehlermeldung anzeigen
|
||||||
|
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt einen Ladeindikator im Chat an
|
||||||
|
*/
|
||||||
|
showLoadingIndicator() {
|
||||||
|
const loadingEl = document.createElement('div');
|
||||||
|
loadingEl.id = 'assistant-loading';
|
||||||
|
loadingEl.className = 'flex justify-start';
|
||||||
|
|
||||||
|
const bubble = document.createElement('div');
|
||||||
|
bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3';
|
||||||
|
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
||||||
|
|
||||||
|
loadingEl.appendChild(bubble);
|
||||||
|
this.chatHistory.appendChild(loadingEl);
|
||||||
|
|
||||||
|
// Scroll zum Ende des Verlaufs
|
||||||
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
|
|
||||||
|
// Stil für den Typing-Indikator
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
background-color: #888;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: typing 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
@keyframes typing {
|
||||||
|
0% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-5px); }
|
||||||
|
100% { transform: translateY(0); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt den Ladeindikator aus dem Chat
|
||||||
|
*/
|
||||||
|
removeLoadingIndicator() {
|
||||||
|
const loadingEl = document.getElementById('assistant-loading');
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
||||||
|
export default ChatGPTAssistant;
|
||||||
95
static/js/modules/cyber-network-init.js
Normal file
95
static/js/modules/cyber-network-init.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Initialisierungsmodul für den CyberNetwork-Hintergrund
|
||||||
|
* Importiert und startet die Animation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import CyberNetwork from './cyber-network.js';
|
||||||
|
|
||||||
|
// Beim Laden des Dokuments starten
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('CyberNetwork: Initialisierung gestartet');
|
||||||
|
|
||||||
|
// Prüfen ob das CSS bereits geladen ist, wenn nicht, dann laden
|
||||||
|
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
|
||||||
|
console.log('CyberNetwork: CSS wird geladen');
|
||||||
|
const cyberNetworkCss = document.createElement('link');
|
||||||
|
cyberNetworkCss.rel = 'stylesheet';
|
||||||
|
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
|
||||||
|
document.head.appendChild(cyberNetworkCss);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container-Element für das Netzwerk finden
|
||||||
|
const container = document.getElementById('cyber-background-container');
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('CyberNetwork: Container gefunden', container);
|
||||||
|
|
||||||
|
// Konfiguration für den Netzwerk-Hintergrund
|
||||||
|
const networkConfig = {
|
||||||
|
container: container,
|
||||||
|
nodeCount: window.innerWidth < 768 ? 15 : 30, // Weniger Nodes auf mobilen Geräten
|
||||||
|
connectionCount: window.innerWidth < 768 ? 25 : 50,
|
||||||
|
packetCount: window.innerWidth < 768 ? 8 : 15,
|
||||||
|
animationSpeed: 1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Netzwerk erstellen und initialisieren
|
||||||
|
const cyberNetwork = new CyberNetwork(networkConfig);
|
||||||
|
cyberNetwork.init();
|
||||||
|
console.log('CyberNetwork: Netzwerk initialisiert');
|
||||||
|
|
||||||
|
// Globale Referenz für Debug-Zwecke
|
||||||
|
window.cyberNetwork = cyberNetwork;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum manuellen Initialisieren, falls notwendig
|
||||||
|
export function initCyberNetwork(config = {}) {
|
||||||
|
console.log('CyberNetwork: Manuelle Initialisierung gestartet');
|
||||||
|
|
||||||
|
// CSS laden, falls nicht vorhanden
|
||||||
|
if (!document.querySelector('link[href*="cybernetwork-bg.css"]')) {
|
||||||
|
console.log('CyberNetwork: CSS wird geladen (manuell)');
|
||||||
|
const cyberNetworkCss = document.createElement('link');
|
||||||
|
cyberNetworkCss.rel = 'stylesheet';
|
||||||
|
cyberNetworkCss.href = '/static/css/src/cybernetwork-bg.css';
|
||||||
|
document.head.appendChild(cyberNetworkCss);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container-Element für das Netzwerk finden
|
||||||
|
const container = document.getElementById('cyber-background-container');
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
console.error('CyberNetwork: Container #cyber-background-container nicht gefunden!');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bestehende Instanz zurücksetzen, falls vorhanden
|
||||||
|
if (window.cyberNetwork) {
|
||||||
|
console.log('CyberNetwork: Bestehende Instanz wird zurückgesetzt');
|
||||||
|
window.cyberNetwork.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Netzwerk mit benutzerdefinierten Optionen erstellen
|
||||||
|
const networkConfig = {
|
||||||
|
container: container,
|
||||||
|
nodeCount: window.innerWidth < 768 ? 15 : 30,
|
||||||
|
connectionCount: window.innerWidth < 768 ? 25 : 50,
|
||||||
|
packetCount: window.innerWidth < 768 ? 8 : 15,
|
||||||
|
animationSpeed: 1.0,
|
||||||
|
...config
|
||||||
|
};
|
||||||
|
|
||||||
|
// Neue Instanz erstellen und initialisieren
|
||||||
|
const cyberNetwork = new CyberNetwork(networkConfig);
|
||||||
|
cyberNetwork.init();
|
||||||
|
console.log('CyberNetwork: Netzwerk manuell initialisiert');
|
||||||
|
|
||||||
|
// Globale Referenz aktualisieren
|
||||||
|
window.cyberNetwork = cyberNetwork;
|
||||||
|
|
||||||
|
return cyberNetwork;
|
||||||
|
}
|
||||||
240
static/js/modules/cyber-network.js
Normal file
240
static/js/modules/cyber-network.js
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* Cyber Network Background Animation
|
||||||
|
* Generiert dynamisch ein animiertes Netzwerk für den Hintergrund
|
||||||
|
*/
|
||||||
|
|
||||||
|
class CyberNetwork {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = {
|
||||||
|
container: options.container || document.body,
|
||||||
|
nodeCount: options.nodeCount || 30,
|
||||||
|
connectionCount: options.connectionCount || 50,
|
||||||
|
packetCount: options.packetCount || 15,
|
||||||
|
animationSpeed: options.animationSpeed || 1.0,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.nodes = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.packets = [];
|
||||||
|
this.initialized = false;
|
||||||
|
|
||||||
|
this.containerElement = null;
|
||||||
|
this.networkGridElement = null;
|
||||||
|
this.glowOverlayElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
// Container erstellen
|
||||||
|
this.containerElement = document.createElement('div');
|
||||||
|
this.containerElement.className = 'cyber-network-bg';
|
||||||
|
|
||||||
|
// Grid erstellen
|
||||||
|
this.networkGridElement = document.createElement('div');
|
||||||
|
this.networkGridElement.className = 'network-grid';
|
||||||
|
this.containerElement.appendChild(this.networkGridElement);
|
||||||
|
|
||||||
|
// Glow Overlay erstellen
|
||||||
|
this.glowOverlayElement = document.createElement('div');
|
||||||
|
this.glowOverlayElement.className = 'glow-overlay';
|
||||||
|
this.containerElement.appendChild(this.glowOverlayElement);
|
||||||
|
|
||||||
|
// Nodes generieren
|
||||||
|
this.generateNodes();
|
||||||
|
|
||||||
|
// Connections generieren
|
||||||
|
this.generateConnections();
|
||||||
|
|
||||||
|
// Data packets generieren
|
||||||
|
this.generateDataPackets();
|
||||||
|
|
||||||
|
// Container zum DOM hinzufügen
|
||||||
|
if (typeof this.options.container === 'string') {
|
||||||
|
const container = document.querySelector(this.options.container);
|
||||||
|
if (container) {
|
||||||
|
container.appendChild(this.containerElement);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(this.containerElement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.options.container.appendChild(this.containerElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
// Animation starten
|
||||||
|
window.addEventListener('resize', this.handleResize.bind(this));
|
||||||
|
this.startAnimationCycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateNodes() {
|
||||||
|
const containerWidth = window.innerWidth;
|
||||||
|
const containerHeight = window.innerHeight;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.options.nodeCount; i++) {
|
||||||
|
const x = Math.random() * containerWidth;
|
||||||
|
const y = Math.random() * containerHeight;
|
||||||
|
|
||||||
|
const node = document.createElement('div');
|
||||||
|
node.className = 'node';
|
||||||
|
node.style.left = `${x}px`;
|
||||||
|
node.style.top = `${y}px`;
|
||||||
|
|
||||||
|
// Größen-Variation für visuelle Tiefe
|
||||||
|
const size = 2 + Math.random() * 4;
|
||||||
|
node.style.width = `${size}px`;
|
||||||
|
node.style.height = `${size}px`;
|
||||||
|
|
||||||
|
// Speichern der Position für spätere Referenz
|
||||||
|
node._data = { x, y, size };
|
||||||
|
|
||||||
|
this.containerElement.appendChild(node);
|
||||||
|
this.nodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateConnections() {
|
||||||
|
for (let i = 0; i < this.options.connectionCount; i++) {
|
||||||
|
// Zufällige Nodes auswählen
|
||||||
|
const startNodeIndex = Math.floor(Math.random() * this.nodes.length);
|
||||||
|
let endNodeIndex;
|
||||||
|
do {
|
||||||
|
endNodeIndex = Math.floor(Math.random() * this.nodes.length);
|
||||||
|
} while (endNodeIndex === startNodeIndex);
|
||||||
|
|
||||||
|
const startNode = this.nodes[startNodeIndex];
|
||||||
|
const endNode = this.nodes[endNodeIndex];
|
||||||
|
const startData = startNode._data;
|
||||||
|
const endData = endNode._data;
|
||||||
|
|
||||||
|
// Verbindung erstellen
|
||||||
|
const connection = document.createElement('div');
|
||||||
|
connection.className = 'connection';
|
||||||
|
|
||||||
|
// Position und Rotation berechnen
|
||||||
|
const dx = endData.x - startData.x;
|
||||||
|
const dy = endData.y - startData.y;
|
||||||
|
const length = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||||
|
|
||||||
|
connection.style.width = `${length}px`;
|
||||||
|
connection.style.left = `${startData.x}px`;
|
||||||
|
connection.style.top = `${startData.y}px`;
|
||||||
|
connection.style.transform = `rotate(${angle}deg)`;
|
||||||
|
|
||||||
|
// Variation in der Animations-Geschwindigkeit
|
||||||
|
connection.style.animationDuration = `${3 + Math.random() * 4}s`;
|
||||||
|
|
||||||
|
// Speichern der verbundenen Nodes
|
||||||
|
connection._data = {
|
||||||
|
startNode: startNodeIndex,
|
||||||
|
endNode: endNodeIndex,
|
||||||
|
length
|
||||||
|
};
|
||||||
|
|
||||||
|
this.containerElement.appendChild(connection);
|
||||||
|
this.connections.push(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateDataPackets() {
|
||||||
|
for (let i = 0; i < this.options.packetCount; i++) {
|
||||||
|
this.createNewDataPacket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewDataPacket() {
|
||||||
|
if (this.connections.length === 0) return;
|
||||||
|
|
||||||
|
// Zufällige Verbindung auswählen
|
||||||
|
const connectionIndex = Math.floor(Math.random() * this.connections.length);
|
||||||
|
const connection = this.connections[connectionIndex];
|
||||||
|
const connectionData = connection._data;
|
||||||
|
|
||||||
|
const startNode = this.nodes[connectionData.startNode];
|
||||||
|
const startData = startNode._data;
|
||||||
|
|
||||||
|
// Data Packet erstellen
|
||||||
|
const packet = document.createElement('div');
|
||||||
|
packet.className = 'data-packet';
|
||||||
|
|
||||||
|
// Position auf dem Startknoten
|
||||||
|
packet.style.left = `${startData.x}px`;
|
||||||
|
packet.style.top = `${startData.y}px`;
|
||||||
|
|
||||||
|
// Zufällige Geschwindigkeit
|
||||||
|
const travelTime = (4 + Math.random() * 4) / this.options.animationSpeed;
|
||||||
|
packet.style.setProperty('--travel-time', `${travelTime}s`);
|
||||||
|
|
||||||
|
// Ziel-Koordinaten berechnen
|
||||||
|
const endNode = this.nodes[connectionData.endNode];
|
||||||
|
const endData = endNode._data;
|
||||||
|
const travelX = endData.x - startData.x;
|
||||||
|
const travelY = endData.y - startData.y;
|
||||||
|
|
||||||
|
packet.style.setProperty('--travel-x', `${travelX}px`);
|
||||||
|
packet.style.setProperty('--travel-y', `${travelY}px`);
|
||||||
|
|
||||||
|
// Farb-Variation
|
||||||
|
if (Math.random() > 0.5) {
|
||||||
|
packet.style.background = 'rgba(76, 223, 255, 0.8)'; // Akzentfarbe
|
||||||
|
}
|
||||||
|
|
||||||
|
this.containerElement.appendChild(packet);
|
||||||
|
this.packets.push(packet);
|
||||||
|
|
||||||
|
// Nach Ende der Animation neues Paket erstellen
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.containerElement.contains(packet)) {
|
||||||
|
this.containerElement.removeChild(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.packets.indexOf(packet);
|
||||||
|
if (index > -1) {
|
||||||
|
this.packets.splice(index, 1);
|
||||||
|
this.createNewDataPacket();
|
||||||
|
}
|
||||||
|
}, travelTime * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
|
// Bei Größenänderung alles neu generieren
|
||||||
|
this.reset();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
|
// Alle Elemente entfernen
|
||||||
|
this.nodes.forEach(node => node.remove());
|
||||||
|
this.connections.forEach(connection => connection.remove());
|
||||||
|
this.packets.forEach(packet => packet.remove());
|
||||||
|
|
||||||
|
this.nodes = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.packets = [];
|
||||||
|
|
||||||
|
if (this.containerElement) {
|
||||||
|
this.containerElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimationCycle() {
|
||||||
|
// Regelmäßig neue Pakete erstellen für mehr Dynamik
|
||||||
|
setInterval(() => {
|
||||||
|
if (this.packets.length < this.options.packetCount * 1.5) {
|
||||||
|
this.createNewDataPacket();
|
||||||
|
}
|
||||||
|
}, 1000 / this.options.animationSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exportieren als Modul
|
||||||
|
export default CyberNetwork;
|
||||||
719
static/js/modules/mindmap-page.js
Normal file
719
static/js/modules/mindmap-page.js
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
/**
|
||||||
|
* Mindmap-Seite JavaScript
|
||||||
|
* Spezifische Funktionen für die Mindmap-Seite
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Registriere den Initialisierer im MindMap-Objekt
|
||||||
|
if (window.MindMap) {
|
||||||
|
window.MindMap.pageInitializers.mindmap = initMindmapPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob wir auf der Mindmap-Seite sind und initialisiere
|
||||||
|
if (document.body.dataset.page === 'mindmap') {
|
||||||
|
initMindmapPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisiert die Mindmap-Seite
|
||||||
|
*/
|
||||||
|
function initMindmapPage() {
|
||||||
|
const mindmapContainer = document.getElementById('mindmap-container');
|
||||||
|
const thoughtsContainer = document.getElementById('thoughts-container');
|
||||||
|
|
||||||
|
if (!mindmapContainer) {
|
||||||
|
console.error('Mindmap-Container nicht gefunden!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfe, ob D3.js geladen ist
|
||||||
|
if (typeof d3 === 'undefined') {
|
||||||
|
console.error('D3.js ist nicht geladen!');
|
||||||
|
mindmapContainer.innerHTML = `
|
||||||
|
<div class="glass-effect p-6 text-center">
|
||||||
|
<div class="text-red-500 mb-4">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl">D3.js konnte nicht geladen werden. Bitte laden Sie die Seite neu.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle die Mindmap-Visualisierung
|
||||||
|
const mindmap = new MindMapVisualization('#mindmap-container', {
|
||||||
|
height: 600,
|
||||||
|
onNodeClick: handleNodeClick
|
||||||
|
});
|
||||||
|
|
||||||
|
// Globale Referenz für die Zoom-Buttons erstellen
|
||||||
|
window.mindmapInstance = mindmap;
|
||||||
|
|
||||||
|
// Lade die Mindmap-Daten
|
||||||
|
mindmap.loadData();
|
||||||
|
|
||||||
|
// Suchfunktion für die Mindmap
|
||||||
|
const searchInput = document.getElementById('mindmap-search');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', function(e) {
|
||||||
|
mindmap.filterBySearchTerm(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behandelt Klicks auf Mindmap-Knoten
|
||||||
|
*/
|
||||||
|
async function handleNodeClick(node) {
|
||||||
|
if (!thoughtsContainer) return;
|
||||||
|
|
||||||
|
// Zeige Lade-Animation
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="flex justify-center items-center p-12">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Lade Gedanken für den ausgewählten Knoten
|
||||||
|
const response = await fetch(`/api/nodes/${node.id}/thoughts`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thoughts = await response.json();
|
||||||
|
|
||||||
|
// Gedanken anzeigen
|
||||||
|
renderThoughts(thoughts, node.name);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Gedanken:', error);
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="glass-effect p-6 text-center">
|
||||||
|
<div class="text-red-500 mb-4">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl">Fehler beim Laden der Gedanken.</p>
|
||||||
|
<p class="text-gray-300">Bitte versuchen Sie es später erneut.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert die Gedanken in den Container
|
||||||
|
*/
|
||||||
|
function renderThoughts(thoughts, nodeName) {
|
||||||
|
// Wenn keine Gedanken vorhanden sind
|
||||||
|
if (thoughts.length === 0) {
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="glass-effect p-6 text-center">
|
||||||
|
<div class="text-blue-400 mb-4">
|
||||||
|
<i class="fa-solid fa-info-circle text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl">Keine Gedanken für "${nodeName}" vorhanden.</p>
|
||||||
|
<button id="add-thought-btn" class="btn-primary mt-4">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i> Gedanke hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event-Listener für den Button
|
||||||
|
document.getElementById('add-thought-btn').addEventListener('click', () => {
|
||||||
|
openAddThoughtModal(nodeName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gedanken anzeigen
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold text-white">Gedanken zu "${nodeName}"</h2>
|
||||||
|
<button id="add-thought-btn" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i> Neuer Gedanke
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4" id="thoughts-grid"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Button-Event-Listener
|
||||||
|
document.getElementById('add-thought-btn').addEventListener('click', () => {
|
||||||
|
openAddThoughtModal(nodeName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gedanken-Karten rendern
|
||||||
|
const thoughtsGrid = document.getElementById('thoughts-grid');
|
||||||
|
thoughts.forEach((thought, index) => {
|
||||||
|
const card = createThoughtCard(thought);
|
||||||
|
|
||||||
|
// Animation verzögern für gestaffeltes Erscheinen
|
||||||
|
setTimeout(() => {
|
||||||
|
card.classList.add('opacity-100');
|
||||||
|
card.classList.remove('opacity-0', 'translate-y-4');
|
||||||
|
}, index * 100);
|
||||||
|
|
||||||
|
thoughtsGrid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt eine Gedanken-Karte
|
||||||
|
*/
|
||||||
|
function createThoughtCard(thought) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card transition-all duration-300 opacity-0 translate-y-4 transform hover:shadow-lg border-l-4';
|
||||||
|
card.style.borderLeftColor = thought.color_code || '#4080ff';
|
||||||
|
|
||||||
|
// Karten-Inhalt
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<h3 class="text-lg font-bold text-white">${thought.title}</h3>
|
||||||
|
<div class="text-sm text-gray-400">${thought.timestamp}</div>
|
||||||
|
</div>
|
||||||
|
<div class="prose dark:prose-invert mt-2">
|
||||||
|
<p>${thought.content}</p>
|
||||||
|
</div>
|
||||||
|
${thought.keywords ? `
|
||||||
|
<div class="flex flex-wrap gap-1 mt-3">
|
||||||
|
${thought.keywords.split(',').map(keyword =>
|
||||||
|
`<span class="px-2 py-1 text-xs rounded-full bg-secondary-700 text-white">${keyword.trim()}</span>`
|
||||||
|
).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="mt-4 flex justify-between items-center">
|
||||||
|
<div class="text-sm text-gray-400">
|
||||||
|
<i class="fa-solid fa-user mr-1"></i> ${thought.author}
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
|
||||||
|
onclick="showComments(${thought.id})">
|
||||||
|
<i class="fa-solid fa-comments mr-1"></i> Kommentare
|
||||||
|
</button>
|
||||||
|
<button class="text-sm px-2 py-1 rounded hover:bg-white/10 transition-colors"
|
||||||
|
onclick="showRelations(${thought.id})">
|
||||||
|
<i class="fa-solid fa-diagram-project mr-1"></i> Beziehungen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet das Modal zum Hinzufügen eines neuen Gedankens
|
||||||
|
*/
|
||||||
|
function openAddThoughtModal(nodeName) {
|
||||||
|
// Node-Information extrahieren
|
||||||
|
let nodeId, nodeTitle;
|
||||||
|
|
||||||
|
if (typeof nodeName === 'string') {
|
||||||
|
// Wenn nur ein String übergeben wurde
|
||||||
|
nodeTitle = nodeName;
|
||||||
|
// Versuche nodeId aus der Mindmap zu finden
|
||||||
|
const nodeElement = d3.selectAll('.node-group').filter(d => d.name === nodeName);
|
||||||
|
if (nodeElement.size() > 0) {
|
||||||
|
nodeId = nodeElement.datum().id;
|
||||||
|
}
|
||||||
|
} else if (typeof nodeName === 'object') {
|
||||||
|
// Wenn ein Node-Objekt übergeben wurde
|
||||||
|
nodeId = nodeName.id;
|
||||||
|
nodeTitle = nodeName.name;
|
||||||
|
} else {
|
||||||
|
console.error('Ungültiger Node-Parameter', nodeName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal-Struktur erstellen
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="modal-backdrop"></div>
|
||||||
|
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10 transform transition-all">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-xl font-bold text-white flex items-center">
|
||||||
|
<span class="w-3 h-3 rounded-full bg-primary-400 mr-2"></span>
|
||||||
|
Neuer Gedanke zu "${nodeTitle}"
|
||||||
|
</h3>
|
||||||
|
<button id="close-modal-btn" class="text-gray-400 hover:text-white transition-colors">
|
||||||
|
<i class="fa-solid fa-xmark text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="add-thought-form" class="space-y-4">
|
||||||
|
<input type="hidden" id="node_id" name="node_id" value="${nodeId || ''}">
|
||||||
|
<div>
|
||||||
|
<label for="title" class="block text-sm font-medium text-gray-300">Titel</label>
|
||||||
|
<input type="text" id="title" name="title" required
|
||||||
|
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="content" class="block text-sm font-medium text-gray-300">Inhalt</label>
|
||||||
|
<textarea id="content" name="content" rows="5" required
|
||||||
|
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="keywords" class="block text-sm font-medium text-gray-300">Schlüsselwörter (kommagetrennt)</label>
|
||||||
|
<input type="text" id="keywords" name="keywords"
|
||||||
|
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="abstract" class="block text-sm font-medium text-gray-300">Zusammenfassung (optional)</label>
|
||||||
|
<textarea id="abstract" name="abstract" rows="2"
|
||||||
|
class="mt-1 block w-full rounded-md bg-dark-700 border border-dark-500 text-white p-2.5 focus:ring-2 focus:ring-primary-500 focus:border-transparent"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="color_code" class="block text-sm font-medium text-gray-300">Farbcode</label>
|
||||||
|
<div class="flex space-x-2 mt-1">
|
||||||
|
<input type="color" id="color_code" name="color_code" value="#4080ff"
|
||||||
|
class="h-10 w-10 rounded bg-dark-700 border border-dark-500">
|
||||||
|
<select id="predefined_colors"
|
||||||
|
class="block flex-grow rounded-md bg-dark-700 border border-dark-500 text-white p-2.5">
|
||||||
|
<option value="#4080ff">Blau</option>
|
||||||
|
<option value="#a040ff">Lila</option>
|
||||||
|
<option value="#40bf80">Grün</option>
|
||||||
|
<option value="#ff4080">Rot</option>
|
||||||
|
<option value="#ffaa00">Orange</option>
|
||||||
|
<option value="#00ccff">Türkis</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between pt-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="relative">
|
||||||
|
<button type="button" id="open-relation-btn" class="btn-outline text-sm pl-3 pr-9">
|
||||||
|
<i class="fa-solid fa-diagram-project mr-2"></i> Verbindung
|
||||||
|
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
|
||||||
|
</button>
|
||||||
|
<div id="relation-menu" class="absolute left-0 mt-2 w-60 rounded-md shadow-lg bg-dark-800 ring-1 ring-black ring-opacity-5 z-10 hidden">
|
||||||
|
<div class="py-1">
|
||||||
|
<div class="px-3 py-2 text-xs font-semibold text-gray-400 border-b border-dark-600">BEZIEHUNGSTYPEN</div>
|
||||||
|
<div class="max-h-48 overflow-y-auto">
|
||||||
|
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="supports">
|
||||||
|
<i class="fa-solid fa-circle-arrow-up text-green-400 mr-2"></i> Stützt
|
||||||
|
</button>
|
||||||
|
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="contradicts">
|
||||||
|
<i class="fa-solid fa-circle-arrow-down text-red-400 mr-2"></i> Widerspricht
|
||||||
|
</button>
|
||||||
|
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="builds_upon">
|
||||||
|
<i class="fa-solid fa-arrow-right text-blue-400 mr-2"></i> Baut auf auf
|
||||||
|
</button>
|
||||||
|
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="generalizes">
|
||||||
|
<i class="fa-solid fa-arrow-up-wide-short text-purple-400 mr-2"></i> Verallgemeinert
|
||||||
|
</button>
|
||||||
|
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="specifies">
|
||||||
|
<i class="fa-solid fa-arrow-down-wide-short text-yellow-400 mr-2"></i> Spezifiziert
|
||||||
|
</button>
|
||||||
|
<button type="button" class="relation-type-btn w-full text-left px-4 py-2 text-sm text-white hover:bg-dark-600" data-type="inspires">
|
||||||
|
<i class="fa-solid fa-lightbulb text-amber-400 mr-2"></i> Inspiriert
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="relation_type" name="relation_type" value="">
|
||||||
|
<input type="hidden" id="relation_target" name="relation_target" value="">
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<button type="button" id="cancel-btn" class="btn-outline">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-save mr-2"></i> Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Focus auf das erste Feld setzen
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.querySelector('#title').focus();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Event-Listener hinzufügen
|
||||||
|
modal.querySelector('#modal-backdrop').addEventListener('click', closeModal);
|
||||||
|
modal.querySelector('#close-modal-btn').addEventListener('click', closeModal);
|
||||||
|
modal.querySelector('#cancel-btn').addEventListener('click', closeModal);
|
||||||
|
|
||||||
|
// Farbauswahl-Event-Listener
|
||||||
|
const colorInput = modal.querySelector('#color_code');
|
||||||
|
const predefinedColors = modal.querySelector('#predefined_colors');
|
||||||
|
|
||||||
|
predefinedColors.addEventListener('change', function() {
|
||||||
|
colorInput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Beziehungsmenü-Funktionalität
|
||||||
|
const relationBtn = modal.querySelector('#open-relation-btn');
|
||||||
|
const relationMenu = modal.querySelector('#relation-menu');
|
||||||
|
|
||||||
|
relationBtn.addEventListener('click', function() {
|
||||||
|
relationMenu.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick außerhalb des Menüs schließt es
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
if (!relationBtn.contains(event.target) && !relationMenu.contains(event.target)) {
|
||||||
|
relationMenu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Beziehungstyp-Auswahl
|
||||||
|
const relationTypeBtns = modal.querySelectorAll('.relation-type-btn');
|
||||||
|
const relationTypeInput = modal.querySelector('#relation_type');
|
||||||
|
|
||||||
|
relationTypeBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const relationType = this.dataset.type;
|
||||||
|
relationTypeInput.value = relationType;
|
||||||
|
|
||||||
|
// Sichtbare Anzeige aktualisieren
|
||||||
|
relationBtn.innerHTML = `
|
||||||
|
<i class="fa-solid fa-diagram-project mr-2"></i>
|
||||||
|
${this.innerText.trim()}
|
||||||
|
<i class="fa-solid fa-chevron-down absolute right-3 top-1/2 transform -translate-y-1/2"></i>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Menü schließen
|
||||||
|
relationMenu.classList.add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form-Submit-Handler
|
||||||
|
const form = modal.querySelector('#add-thought-form');
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const thoughtData = {
|
||||||
|
node_id: formData.get('node_id'),
|
||||||
|
title: formData.get('title'),
|
||||||
|
content: formData.get('content'),
|
||||||
|
keywords: formData.get('keywords'),
|
||||||
|
abstract: formData.get('abstract'),
|
||||||
|
color_code: formData.get('color_code'),
|
||||||
|
relation_type: formData.get('relation_type'),
|
||||||
|
relation_target: formData.get('relation_target')
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/thoughts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(thoughtData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Speichern des Gedankens.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal schließen
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
// Gedanken neu laden
|
||||||
|
if (nodeId) {
|
||||||
|
handleNodeClick({ id: nodeId, name: nodeTitle });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erfolgsbenachrichtigung
|
||||||
|
if (window.MindMap && window.MindMap.showNotification) {
|
||||||
|
MindMap.showNotification('Gedanke erfolgreich gespeichert.', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Speichern:', error);
|
||||||
|
if (window.MindMap && window.MindMap.showNotification) {
|
||||||
|
MindMap.showNotification('Fehler beim Speichern des Gedankens.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal schließen
|
||||||
|
function closeModal() {
|
||||||
|
modal.classList.add('opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt die Kommentare zu einem Gedanken an
|
||||||
|
*/
|
||||||
|
window.showComments = async function(thoughtId) {
|
||||||
|
try {
|
||||||
|
// Lade-Animation erstellen
|
||||||
|
const modal = createModalWithLoading('Kommentare werden geladen...');
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Kommentare laden
|
||||||
|
const response = await fetch(`/api/comments/${thoughtId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = await response.json();
|
||||||
|
|
||||||
|
// Modal mit Kommentaren aktualisieren
|
||||||
|
updateModalWithComments(modal, comments, thoughtId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Kommentare:', error);
|
||||||
|
MindMap.showNotification('Fehler beim Laden der Kommentare.', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt die Beziehungen eines Gedankens an
|
||||||
|
*/
|
||||||
|
window.showRelations = async function(thoughtId) {
|
||||||
|
try {
|
||||||
|
// Lade-Animation erstellen
|
||||||
|
const modal = createModalWithLoading('Beziehungen werden geladen...');
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Beziehungen laden
|
||||||
|
const response = await fetch(`/api/thoughts/${thoughtId}/relations`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const relations = await response.json();
|
||||||
|
|
||||||
|
// Modal mit Beziehungen aktualisieren
|
||||||
|
updateModalWithRelations(modal, relations, thoughtId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Beziehungen:', error);
|
||||||
|
MindMap.showNotification('Fehler beim Laden der Beziehungen.', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein Modal mit Lade-Animation
|
||||||
|
*/
|
||||||
|
function createModalWithLoading(loadingText) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 z-50 flex items-center justify-center p-4';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="absolute inset-0 bg-black/50" id="modal-backdrop"></div>
|
||||||
|
<div class="glass-effect relative rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto z-10">
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-400"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg text-white">${loadingText}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event-Listener zum Schließen
|
||||||
|
modal.querySelector('#modal-backdrop').addEventListener('click', () => {
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert das Modal mit Kommentaren
|
||||||
|
*/
|
||||||
|
function updateModalWithComments(modal, comments, thoughtId) {
|
||||||
|
const modalContent = modal.querySelector('.glass-effect');
|
||||||
|
|
||||||
|
modalContent.innerHTML = `
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-xl font-bold text-white">Kommentare</h3>
|
||||||
|
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
|
||||||
|
<i class="fa-solid fa-xmark text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comments-list mb-6 space-y-4">
|
||||||
|
${comments.length === 0 ?
|
||||||
|
'<div class="text-center text-gray-400 py-4">Keine Kommentare vorhanden.</div>' :
|
||||||
|
comments.map(comment => `
|
||||||
|
<div class="glass-effect p-3 rounded">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="font-medium text-white">${comment.author}</div>
|
||||||
|
<div class="text-xs text-gray-400">${comment.timestamp}</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-gray-200">${comment.content}</p>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="comment-form" class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label for="comment-content" class="block text-sm font-medium text-gray-300">Neuer Kommentar</label>
|
||||||
|
<textarea id="comment-content" name="content" rows="3" required
|
||||||
|
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end pt-2">
|
||||||
|
<button type="submit" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-paper-plane mr-2"></i> Senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event-Listener hinzufügen
|
||||||
|
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kommentar-Formular
|
||||||
|
const form = modalContent.querySelector('#comment-form');
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const content = form.elements.content.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/comments', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
thought_id: thoughtId,
|
||||||
|
content: content
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Speichern des Kommentars.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal schließen
|
||||||
|
modal.remove();
|
||||||
|
|
||||||
|
// Erfolgsbenachrichtigung
|
||||||
|
MindMap.showNotification('Kommentar erfolgreich gespeichert.', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Speichern des Kommentars:', error);
|
||||||
|
MindMap.showNotification('Fehler beim Speichern des Kommentars.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert das Modal mit Beziehungen
|
||||||
|
*/
|
||||||
|
function updateModalWithRelations(modal, relations, thoughtId) {
|
||||||
|
const modalContent = modal.querySelector('.glass-effect');
|
||||||
|
|
||||||
|
modalContent.innerHTML = `
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-xl font-bold text-white">Beziehungen</h3>
|
||||||
|
<button id="close-modal-btn" class="text-gray-400 hover:text-white">
|
||||||
|
<i class="fa-solid fa-xmark text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relations-list mb-6 space-y-4">
|
||||||
|
${relations.length === 0 ?
|
||||||
|
'<div class="text-center text-gray-400 py-4">Keine Beziehungen vorhanden.</div>' :
|
||||||
|
relations.map(relation => `
|
||||||
|
<div class="glass-effect p-3 rounded">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="inline-block px-2 py-1 rounded-full text-xs font-medium bg-primary-600 text-white">
|
||||||
|
${relation.relation_type}
|
||||||
|
</span>
|
||||||
|
<div class="ml-3">
|
||||||
|
<div class="text-white">Ziel: Gedanke #${relation.target_id}</div>
|
||||||
|
<div class="text-xs text-gray-400">Erstellt von ${relation.created_by} am ${relation.created_at}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="relation-form" class="space-y-3">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="target_id" class="block text-sm font-medium text-gray-300">Ziel-Gedanke ID</label>
|
||||||
|
<input type="number" id="target_id" name="target_id" required
|
||||||
|
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="relation_type" class="block text-sm font-medium text-gray-300">Beziehungstyp</label>
|
||||||
|
<select id="relation_type" name="relation_type" required
|
||||||
|
class="mt-1 block w-full rounded-md bg-dark-700 border-dark-500 text-white">
|
||||||
|
<option value="SUPPORTS">Stützt</option>
|
||||||
|
<option value="CONTRADICTS">Widerspricht</option>
|
||||||
|
<option value="BUILDS_UPON">Baut auf auf</option>
|
||||||
|
<option value="GENERALIZES">Verallgemeinert</option>
|
||||||
|
<option value="SPECIFIES">Spezifiziert</option>
|
||||||
|
<option value="INSPIRES">Inspiriert</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end pt-2">
|
||||||
|
<button type="submit" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i> Beziehung erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event-Listener hinzufügen
|
||||||
|
modalContent.querySelector('#close-modal-btn').addEventListener('click', () => {
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Beziehungs-Formular
|
||||||
|
const form = modalContent.querySelector('#relation-form');
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
source_id: thoughtId,
|
||||||
|
target_id: parseInt(form.elements.target_id.value),
|
||||||
|
relation_type: form.elements.relation_type.value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/relations', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Fehler beim Erstellen der Beziehung.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal schließen
|
||||||
|
modal.remove();
|
||||||
|
|
||||||
|
// Erfolgsbenachrichtigung
|
||||||
|
MindMap.showNotification('Beziehung erfolgreich erstellt.', 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen der Beziehung:', error);
|
||||||
|
MindMap.showNotification('Fehler beim Erstellen der Beziehung.', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
777
static/js/modules/mindmap.js
Normal file
777
static/js/modules/mindmap.js
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
/**
|
||||||
|
* MindMap D3.js Modul
|
||||||
|
* Visualisiert die Mindmap mit D3.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MindMapVisualization {
|
||||||
|
constructor(containerSelector, options = {}) {
|
||||||
|
this.containerSelector = containerSelector;
|
||||||
|
this.container = d3.select(containerSelector);
|
||||||
|
this.width = options.width || this.container.node().clientWidth || 800;
|
||||||
|
this.height = options.height || 600;
|
||||||
|
this.nodeRadius = options.nodeRadius || 14;
|
||||||
|
this.selectedNodeRadius = options.selectedNodeRadius || 20;
|
||||||
|
this.linkDistance = options.linkDistance || 150;
|
||||||
|
this.chargeStrength = options.chargeStrength || -900;
|
||||||
|
this.centerForce = options.centerForce || 0.15;
|
||||||
|
this.onNodeClick = options.onNodeClick || ((node) => console.log('Node clicked:', node));
|
||||||
|
|
||||||
|
this.nodes = [];
|
||||||
|
this.links = [];
|
||||||
|
this.simulation = null;
|
||||||
|
this.svg = null;
|
||||||
|
this.linkElements = null;
|
||||||
|
this.nodeElements = null;
|
||||||
|
this.textElements = null;
|
||||||
|
this.tooltipEnabled = options.tooltipEnabled !== undefined ? options.tooltipEnabled : true;
|
||||||
|
|
||||||
|
this.mouseoverNode = null;
|
||||||
|
this.selectedNode = null;
|
||||||
|
|
||||||
|
this.zoomFactor = 1;
|
||||||
|
this.tooltipDiv = null;
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
// Lade die gemerkten Knoten
|
||||||
|
this.bookmarkedNodes = this.loadBookmarkedNodes();
|
||||||
|
|
||||||
|
// Sicherstellen, dass der Container bereit ist
|
||||||
|
if (this.container.node()) {
|
||||||
|
this.init();
|
||||||
|
this.setupDefaultNodes();
|
||||||
|
|
||||||
|
// Sofortige Datenladung
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.loadData();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.error('Mindmap-Container nicht gefunden:', containerSelector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standardknoten als Fallback einrichten, falls die API nicht reagiert
|
||||||
|
setupDefaultNodes() {
|
||||||
|
// Basis-Mindmap mit Hauptthemen
|
||||||
|
const defaultNodes = [
|
||||||
|
{ id: "root", name: "Wissen", description: "Zentrale Wissensbasis", thought_count: 0 },
|
||||||
|
{ id: "philosophy", name: "Philosophie", description: "Philosophisches Denken", thought_count: 0 },
|
||||||
|
{ id: "science", name: "Wissenschaft", description: "Wissenschaftliche Erkenntnisse", thought_count: 0 },
|
||||||
|
{ id: "technology", name: "Technologie", description: "Technologische Entwicklungen", thought_count: 0 },
|
||||||
|
{ id: "arts", name: "Künste", description: "Künstlerische Ausdrucksformen", thought_count: 0 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultLinks = [
|
||||||
|
{ source: "root", target: "philosophy" },
|
||||||
|
{ source: "root", target: "science" },
|
||||||
|
{ source: "root", target: "technology" },
|
||||||
|
{ source: "root", target: "arts" }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Als Fallback verwenden, falls die API fehlschlägt
|
||||||
|
this.defaultNodes = defaultNodes;
|
||||||
|
this.defaultLinks = defaultLinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// SVG erstellen, wenn noch nicht vorhanden
|
||||||
|
if (!this.svg) {
|
||||||
|
// Container zuerst leeren
|
||||||
|
this.container.html('');
|
||||||
|
|
||||||
|
this.svg = this.container
|
||||||
|
.append('svg')
|
||||||
|
.attr('width', '100%')
|
||||||
|
.attr('height', this.height)
|
||||||
|
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
||||||
|
.attr('class', 'mindmap-svg')
|
||||||
|
.call(
|
||||||
|
d3.zoom()
|
||||||
|
.scaleExtent([0.1, 5])
|
||||||
|
.on('zoom', (event) => {
|
||||||
|
this.handleZoom(event.transform);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hauptgruppe für alles, was zoom-transformierbar ist
|
||||||
|
this.g = this.svg.append('g');
|
||||||
|
|
||||||
|
// Tooltip initialisieren
|
||||||
|
if (!d3.select('body').select('.node-tooltip').size()) {
|
||||||
|
this.tooltipDiv = d3.select('body')
|
||||||
|
.append('div')
|
||||||
|
.attr('class', 'node-tooltip')
|
||||||
|
.style('opacity', 0)
|
||||||
|
.style('position', 'absolute')
|
||||||
|
.style('pointer-events', 'none')
|
||||||
|
.style('background', 'rgba(20, 20, 40, 0.9)')
|
||||||
|
.style('color', '#ffffff')
|
||||||
|
.style('border', '1px solid rgba(160, 80, 255, 0.2)')
|
||||||
|
.style('border-radius', '6px')
|
||||||
|
.style('padding', '8px 12px')
|
||||||
|
.style('font-size', '14px')
|
||||||
|
.style('max-width', '250px')
|
||||||
|
.style('box-shadow', '0 10px 25px rgba(0, 0, 0, 0.5), 0 0 10px rgba(160, 80, 255, 0.2)');
|
||||||
|
} else {
|
||||||
|
this.tooltipDiv = d3.select('body').select('.node-tooltip');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force-Simulation initialisieren
|
||||||
|
this.simulation = d3.forceSimulation()
|
||||||
|
.force('link', d3.forceLink().id(d => d.id).distance(this.linkDistance))
|
||||||
|
.force('charge', d3.forceManyBody().strength(this.chargeStrength))
|
||||||
|
.force('center', d3.forceCenter(this.width / 2, this.height / 2).strength(this.centerForce))
|
||||||
|
.force('collision', d3.forceCollide().radius(this.nodeRadius * 2));
|
||||||
|
|
||||||
|
// Globale Mindmap-Instanz für externe Zugriffe setzen
|
||||||
|
window.mindmapInstance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleZoom(transform) {
|
||||||
|
this.g.attr('transform', transform);
|
||||||
|
this.zoomFactor = transform.k;
|
||||||
|
|
||||||
|
// Knotengröße anpassen, um bei Zoom lesbar zu bleiben
|
||||||
|
if (this.nodeElements) {
|
||||||
|
this.nodeElements
|
||||||
|
.attr('r', d => (d === this.selectedNode ? this.selectedNodeRadius : this.nodeRadius) / Math.sqrt(transform.k));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Textgröße anpassen
|
||||||
|
if (this.textElements) {
|
||||||
|
this.textElements
|
||||||
|
.style('font-size', `${12 / Math.sqrt(transform.k)}px`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadData() {
|
||||||
|
try {
|
||||||
|
// Ladeindikator anzeigen
|
||||||
|
this.showLoading();
|
||||||
|
|
||||||
|
// Verwende sofort die Standarddaten für eine schnelle erste Anzeige
|
||||||
|
this.nodes = [...this.defaultNodes];
|
||||||
|
this.links = [...this.defaultLinks];
|
||||||
|
|
||||||
|
// Visualisierung sofort aktualisieren
|
||||||
|
this.isLoading = false;
|
||||||
|
this.updateVisualization();
|
||||||
|
|
||||||
|
// Status auf bereit setzen - don't wait for API
|
||||||
|
this.container.attr('data-status', 'ready');
|
||||||
|
|
||||||
|
// API-Aufruf mit kürzerem Timeout im Hintergrund durchführen
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 Sekunden Timeout - reduced from 10
|
||||||
|
|
||||||
|
const response = await fetch('/api/mindmap', {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(`HTTP Fehler: ${response.status}, verwende Standarddaten`);
|
||||||
|
return; // Keep using default data
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data || !data.nodes || data.nodes.length === 0) {
|
||||||
|
console.warn('Keine Mindmap-Daten vorhanden, verwende weiterhin Standard-Daten.');
|
||||||
|
return; // Keep using default data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flache Liste von Knoten und Verbindungen erstellen
|
||||||
|
this.nodes = [];
|
||||||
|
this.links = [];
|
||||||
|
this.processHierarchicalData(data.nodes);
|
||||||
|
|
||||||
|
// Visualisierung aktualisieren mit den tatsächlichen Daten
|
||||||
|
this.updateVisualization();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Fehler beim Laden der Mindmap-Daten, verwende Standarddaten:', error);
|
||||||
|
// Already using default data, no action needed
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Kritischer Fehler bei der Mindmap-Darstellung:', error);
|
||||||
|
this.showError('Fehler beim Laden der Mindmap-Daten. Bitte laden Sie die Seite neu.');
|
||||||
|
this.container.attr('data-status', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
// Element nur leeren, wenn es noch kein SVG enthält
|
||||||
|
if (!this.container.select('svg').size()) {
|
||||||
|
this.container.html(`
|
||||||
|
<div class="flex justify-center items-center h-full">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-400 mx-auto mb-4"></div>
|
||||||
|
<p class="text-lg text-white">Mindmap wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processHierarchicalData(hierarchicalNodes, parentId = null) {
|
||||||
|
hierarchicalNodes.forEach(node => {
|
||||||
|
// Knoten hinzufügen, wenn noch nicht vorhanden
|
||||||
|
if (!this.nodes.find(n => n.id === node.id)) {
|
||||||
|
this.nodes.push({
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
description: node.description || '',
|
||||||
|
thought_count: node.thought_count || 0,
|
||||||
|
color: this.generateColorFromString(node.name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbindung zum Elternknoten hinzufügen
|
||||||
|
if (parentId !== null) {
|
||||||
|
this.links.push({
|
||||||
|
source: parentId,
|
||||||
|
target: node.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rekursiv für Kindknoten aufrufen
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
this.processHierarchicalData(node.children, node.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateColorFromString(str) {
|
||||||
|
// Erzeugt eine deterministische Farbe basierend auf dem String
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwende deterministische Farbe aus unserem Farbschema
|
||||||
|
const colors = [
|
||||||
|
'#4080ff', // primary-400
|
||||||
|
'#a040ff', // secondary-400
|
||||||
|
'#205cf5', // primary-500
|
||||||
|
'#8020f5', // secondary-500
|
||||||
|
'#1040e0', // primary-600
|
||||||
|
'#6010e0', // secondary-600
|
||||||
|
];
|
||||||
|
|
||||||
|
return colors[Math.abs(hash) % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVisualization() {
|
||||||
|
// Starte die Visualisierung nur, wenn nicht mehr im Ladezustand
|
||||||
|
if (this.isLoading) return;
|
||||||
|
|
||||||
|
// Container leeren, wenn Diagramm neu erstellt wird
|
||||||
|
if (!this.svg) {
|
||||||
|
this.container.html('');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performance-Optimierung: Deaktiviere Transition während des Datenladens
|
||||||
|
const useTransitions = false;
|
||||||
|
|
||||||
|
// Links (Edges) erstellen
|
||||||
|
this.linkElements = this.g.selectAll('.link')
|
||||||
|
.data(this.links)
|
||||||
|
.join(
|
||||||
|
enter => enter.append('line')
|
||||||
|
.attr('class', 'link')
|
||||||
|
.attr('stroke', '#ffffff30')
|
||||||
|
.attr('stroke-width', 2)
|
||||||
|
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
||||||
|
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
||||||
|
update => update
|
||||||
|
.attr('stroke', '#ffffff30')
|
||||||
|
.attr('stroke-dasharray', d => d.relation_type === 'contradicts' ? '5,5' : null)
|
||||||
|
.attr('marker-end', d => d.relation_type === 'builds_upon' ? 'url(#arrowhead)' : null),
|
||||||
|
exit => exit.remove()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pfeilspitze für gerichtete Beziehungen hinzufügen (falls noch nicht vorhanden)
|
||||||
|
if (!this.svg.select('defs').node()) {
|
||||||
|
const defs = this.svg.append('defs');
|
||||||
|
defs.append('marker')
|
||||||
|
.attr('id', 'arrowhead')
|
||||||
|
.attr('viewBox', '0 -5 10 10')
|
||||||
|
.attr('refX', 20)
|
||||||
|
.attr('refY', 0)
|
||||||
|
.attr('orient', 'auto')
|
||||||
|
.attr('markerWidth', 6)
|
||||||
|
.attr('markerHeight', 6)
|
||||||
|
.append('path')
|
||||||
|
.attr('d', 'M0,-5L10,0L0,5')
|
||||||
|
.attr('fill', '#ffffff50');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified Effekte definieren, falls noch nicht vorhanden
|
||||||
|
if (!this.svg.select('#glow').node()) {
|
||||||
|
const defs = this.svg.select('defs').size() ? this.svg.select('defs') : this.svg.append('defs');
|
||||||
|
|
||||||
|
// Glow-Effekt für Knoten
|
||||||
|
const filter = defs.append('filter')
|
||||||
|
.attr('id', 'glow')
|
||||||
|
.attr('x', '-50%')
|
||||||
|
.attr('y', '-50%')
|
||||||
|
.attr('width', '200%')
|
||||||
|
.attr('height', '200%');
|
||||||
|
|
||||||
|
filter.append('feGaussianBlur')
|
||||||
|
.attr('stdDeviation', '1')
|
||||||
|
.attr('result', 'blur');
|
||||||
|
|
||||||
|
filter.append('feComposite')
|
||||||
|
.attr('in', 'SourceGraphic')
|
||||||
|
.attr('in2', 'blur')
|
||||||
|
.attr('operator', 'over');
|
||||||
|
|
||||||
|
// Blur-Effekt für Schatten
|
||||||
|
const blurFilter = defs.append('filter')
|
||||||
|
.attr('id', 'blur')
|
||||||
|
.attr('x', '-50%')
|
||||||
|
.attr('y', '-50%')
|
||||||
|
.attr('width', '200%')
|
||||||
|
.attr('height', '200%');
|
||||||
|
|
||||||
|
blurFilter.append('feGaussianBlur')
|
||||||
|
.attr('stdDeviation', '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten-Gruppe erstellen/aktualisieren
|
||||||
|
const nodeGroups = this.g.selectAll('.node-group')
|
||||||
|
.data(this.nodes)
|
||||||
|
.join(
|
||||||
|
enter => {
|
||||||
|
const group = enter.append('g')
|
||||||
|
.attr('class', 'node-group')
|
||||||
|
.call(d3.drag()
|
||||||
|
.on('start', (event, d) => this.dragStarted(event, d))
|
||||||
|
.on('drag', (event, d) => this.dragged(event, d))
|
||||||
|
.on('end', (event, d) => this.dragEnded(event, d)));
|
||||||
|
|
||||||
|
// Hintergrundschatten für besseren Kontrast
|
||||||
|
group.append('circle')
|
||||||
|
.attr('class', 'node-shadow')
|
||||||
|
.attr('r', d => this.nodeRadius * 1.2)
|
||||||
|
.attr('fill', 'rgba(0, 0, 0, 0.3)')
|
||||||
|
.attr('filter', 'url(#blur)');
|
||||||
|
|
||||||
|
// Kreis für jeden Knoten
|
||||||
|
group.append('circle')
|
||||||
|
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
||||||
|
.attr('r', this.nodeRadius)
|
||||||
|
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
||||||
|
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
||||||
|
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2)
|
||||||
|
.attr('filter', 'url(#glow)');
|
||||||
|
|
||||||
|
// Text-Label mit besserem Kontrast
|
||||||
|
group.append('text')
|
||||||
|
.attr('class', 'node-label')
|
||||||
|
.attr('dy', '0.35em')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('fill', '#ffffff')
|
||||||
|
.attr('stroke', 'rgba(0, 0, 0, 0.4)')
|
||||||
|
.attr('stroke-width', '0.7px')
|
||||||
|
.attr('paint-order', 'stroke')
|
||||||
|
.style('font-size', '12px')
|
||||||
|
.style('font-weight', '500')
|
||||||
|
.style('pointer-events', 'none')
|
||||||
|
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
||||||
|
|
||||||
|
// Interaktivität hinzufügen
|
||||||
|
group
|
||||||
|
.on('mouseover', (event, d) => this.nodeMouseover(event, d))
|
||||||
|
.on('mouseout', (event, d) => this.nodeMouseout(event, d))
|
||||||
|
.on('click', (event, d) => this.nodeClicked(event, d));
|
||||||
|
|
||||||
|
return group;
|
||||||
|
},
|
||||||
|
update => {
|
||||||
|
// Knoten aktualisieren
|
||||||
|
update.select('.node')
|
||||||
|
.attr('class', d => `node ${this.isNodeBookmarked(d.id) ? 'bookmarked' : ''}`)
|
||||||
|
.attr('fill', d => d.color || this.generateColorFromString(d.name))
|
||||||
|
.attr('stroke', d => this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff50')
|
||||||
|
.attr('stroke-width', d => this.isNodeBookmarked(d.id) ? 3 : 2);
|
||||||
|
|
||||||
|
// Text aktualisieren
|
||||||
|
update.select('.node-label')
|
||||||
|
.text(d => d.name.length > 12 ? d.name.slice(0, 10) + '...' : d.name);
|
||||||
|
|
||||||
|
return update;
|
||||||
|
},
|
||||||
|
exit => exit.remove()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Einzelne Elemente für direkten Zugriff speichern
|
||||||
|
this.nodeElements = this.g.selectAll('.node');
|
||||||
|
this.textElements = this.g.selectAll('.node-label');
|
||||||
|
|
||||||
|
// Performance-Optimierung: Weniger Simulationsschritte für schnellere Stabilisierung
|
||||||
|
this.simulation
|
||||||
|
.nodes(this.nodes)
|
||||||
|
.on('tick', () => this.ticked())
|
||||||
|
.alpha(0.3) // Reduzierter Wert für schnellere Stabilisierung
|
||||||
|
.alphaDecay(0.05); // Erhöhter Wert für schnellere Stabilisierung
|
||||||
|
|
||||||
|
this.simulation.force('link')
|
||||||
|
.links(this.links);
|
||||||
|
|
||||||
|
// Simulation neu starten
|
||||||
|
this.simulation.restart();
|
||||||
|
|
||||||
|
// Update connection counts
|
||||||
|
this.updateConnectionCounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
ticked() {
|
||||||
|
// Linienpositionen aktualisieren
|
||||||
|
this.linkElements
|
||||||
|
.attr('x1', d => d.source.x)
|
||||||
|
.attr('y1', d => d.source.y)
|
||||||
|
.attr('x2', d => d.target.x)
|
||||||
|
.attr('y2', d => d.target.y);
|
||||||
|
|
||||||
|
// Knotenpositionen aktualisieren
|
||||||
|
this.g.selectAll('.node-group')
|
||||||
|
.attr('transform', d => `translate(${d.x}, ${d.y})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragStarted(event, d) {
|
||||||
|
if (!event.active) this.simulation.alphaTarget(0.3).restart();
|
||||||
|
d.fx = d.x;
|
||||||
|
d.fy = d.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragged(event, d) {
|
||||||
|
d.fx = event.x;
|
||||||
|
d.fy = event.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragEnded(event, d) {
|
||||||
|
if (!event.active) this.simulation.alphaTarget(0);
|
||||||
|
d.fx = null;
|
||||||
|
d.fy = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeMouseover(event, d) {
|
||||||
|
this.mouseoverNode = d;
|
||||||
|
|
||||||
|
// Tooltip anzeigen
|
||||||
|
if (this.tooltipEnabled) {
|
||||||
|
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||||
|
const tooltipContent = `
|
||||||
|
<div class="p-2">
|
||||||
|
<strong>${d.name}</strong>
|
||||||
|
${d.description ? `<p class="text-sm text-gray-200 mt-1">${d.description}</p>` : ''}
|
||||||
|
<div class="text-xs text-gray-300 mt-1">
|
||||||
|
Gedanken: ${d.thought_count}
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button id="bookmark-button" class="px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-white"
|
||||||
|
data-nodeid="${d.id}">
|
||||||
|
${isBookmarked ? '<i class="fas fa-bookmark mr-1"></i> Gemerkt' : '<i class="far fa-bookmark mr-1"></i> Merken'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.tooltipDiv
|
||||||
|
.html(tooltipContent)
|
||||||
|
.style('left', (event.pageX + 10) + 'px')
|
||||||
|
.style('top', (event.pageY - 10) + 'px')
|
||||||
|
.transition()
|
||||||
|
.duration(200)
|
||||||
|
.style('opacity', 1);
|
||||||
|
|
||||||
|
// Event-Listener für den Bookmark-Button hinzufügen
|
||||||
|
document.getElementById('bookmark-button').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const nodeId = e.currentTarget.getAttribute('data-nodeid');
|
||||||
|
const isNowBookmarked = this.toggleBookmark(nodeId);
|
||||||
|
|
||||||
|
// Button-Text aktualisieren
|
||||||
|
if (isNowBookmarked) {
|
||||||
|
e.currentTarget.innerHTML = '<i class="fas fa-bookmark mr-1"></i> Gemerkt';
|
||||||
|
} else {
|
||||||
|
e.currentTarget.innerHTML = '<i class="far fa-bookmark mr-1"></i> Merken';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten visuell hervorheben
|
||||||
|
d3.select(event.currentTarget).select('circle')
|
||||||
|
.transition()
|
||||||
|
.duration(200)
|
||||||
|
.attr('r', this.nodeRadius * 1.2)
|
||||||
|
.attr('stroke', this.isNodeBookmarked(d.id) ? '#FFD700' : '#ffffff');
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeMouseout(event, d) {
|
||||||
|
this.mouseoverNode = null;
|
||||||
|
|
||||||
|
// Tooltip ausblenden
|
||||||
|
if (this.tooltipEnabled) {
|
||||||
|
this.tooltipDiv
|
||||||
|
.transition()
|
||||||
|
.duration(200)
|
||||||
|
.style('opacity', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knoten-Stil zurücksetzen, wenn nicht ausgewählt
|
||||||
|
const nodeElement = d3.select(event.currentTarget).select('circle');
|
||||||
|
if (d !== this.selectedNode) {
|
||||||
|
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||||
|
nodeElement
|
||||||
|
.transition()
|
||||||
|
.duration(200)
|
||||||
|
.attr('r', this.nodeRadius)
|
||||||
|
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
||||||
|
.attr('stroke-width', isBookmarked ? 3 : 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeClicked(event, d) {
|
||||||
|
// Frühere Auswahl zurücksetzen
|
||||||
|
if (this.selectedNode && this.selectedNode !== d) {
|
||||||
|
this.g.selectAll('.node')
|
||||||
|
.filter(n => n === this.selectedNode)
|
||||||
|
.transition()
|
||||||
|
.duration(200)
|
||||||
|
.attr('r', this.nodeRadius)
|
||||||
|
.attr('stroke', '#ffffff50');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue Auswahl hervorheben
|
||||||
|
if (this.selectedNode !== d) {
|
||||||
|
this.selectedNode = d;
|
||||||
|
d3.select(event.currentTarget).select('circle')
|
||||||
|
.transition()
|
||||||
|
.duration(200)
|
||||||
|
.attr('r', this.selectedNodeRadius)
|
||||||
|
.attr('stroke', '#ffffff');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback mit Node-Daten aufrufen
|
||||||
|
this.onNodeClick(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
this.container.html(`
|
||||||
|
<div class="w-full text-center p-6">
|
||||||
|
<div class="mb-4 text-red-500">
|
||||||
|
<i class="fas fa-exclamation-triangle text-4xl"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg text-gray-200">${message}</p>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fokussiert die Ansicht auf einen bestimmten Knoten
|
||||||
|
focusNode(nodeId) {
|
||||||
|
const node = this.nodes.find(n => n.id === nodeId);
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
// Simuliere einen Klick auf den Knoten
|
||||||
|
const nodeElement = this.g.selectAll('.node-group')
|
||||||
|
.filter(d => d.id === nodeId);
|
||||||
|
|
||||||
|
nodeElement.dispatch('click');
|
||||||
|
|
||||||
|
// Zentriere den Knoten in der Ansicht
|
||||||
|
const transform = d3.zoomIdentity
|
||||||
|
.translate(this.width / 2, this.height / 2)
|
||||||
|
.scale(1.2)
|
||||||
|
.translate(-node.x, -node.y);
|
||||||
|
|
||||||
|
this.svg.transition()
|
||||||
|
.duration(750)
|
||||||
|
.call(
|
||||||
|
d3.zoom().transform,
|
||||||
|
transform
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtert die Mindmap basierend auf einem Suchbegriff
|
||||||
|
filterBySearchTerm(searchTerm) {
|
||||||
|
if (!searchTerm || searchTerm.trim() === '') {
|
||||||
|
// Alle Knoten anzeigen
|
||||||
|
this.g.selectAll('.node-group')
|
||||||
|
.style('opacity', 1)
|
||||||
|
.style('pointer-events', 'all');
|
||||||
|
|
||||||
|
this.g.selectAll('.link')
|
||||||
|
.style('opacity', 1);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchLower = searchTerm.toLowerCase();
|
||||||
|
const matchingNodes = this.nodes.filter(node =>
|
||||||
|
node.name.toLowerCase().includes(searchLower) ||
|
||||||
|
(node.description && node.description.toLowerCase().includes(searchLower))
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchingNodeIds = new Set(matchingNodes.map(n => n.id));
|
||||||
|
|
||||||
|
// Passende Knoten hervorheben, andere ausblenden
|
||||||
|
this.g.selectAll('.node-group')
|
||||||
|
.style('opacity', d => matchingNodeIds.has(d.id) ? 1 : 0.2)
|
||||||
|
.style('pointer-events', d => matchingNodeIds.has(d.id) ? 'all' : 'none');
|
||||||
|
|
||||||
|
// Verbindungen zwischen passenden Knoten hervorheben
|
||||||
|
this.g.selectAll('.link')
|
||||||
|
.style('opacity', d =>
|
||||||
|
matchingNodeIds.has(d.source.id) && matchingNodeIds.has(d.target.id) ? 1 : 0.1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auf den ersten passenden Knoten fokussieren, wenn vorhanden
|
||||||
|
if (matchingNodes.length > 0) {
|
||||||
|
this.focusNode(matchingNodes[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the thought_count property for each node based on existing connections
|
||||||
|
*/
|
||||||
|
updateConnectionCounts() {
|
||||||
|
// Reset all counts first
|
||||||
|
this.nodes.forEach(node => {
|
||||||
|
// Initialize thought_count if it doesn't exist
|
||||||
|
if (typeof node.thought_count !== 'number') {
|
||||||
|
node.thought_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count connections for this node
|
||||||
|
const connectedNodes = this.getConnectedNodes(node);
|
||||||
|
node.thought_count = connectedNodes.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update UI to show counts
|
||||||
|
this.updateNodeLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visual representation of node labels to include connection counts
|
||||||
|
*/
|
||||||
|
updateNodeLabels() {
|
||||||
|
if (!this.textElements) return;
|
||||||
|
|
||||||
|
this.textElements.text(d => {
|
||||||
|
if (d.thought_count > 0) {
|
||||||
|
return `${d.name} (${d.thought_count})`;
|
||||||
|
}
|
||||||
|
return d.name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new connection between nodes and updates the counts
|
||||||
|
*/
|
||||||
|
addConnection(sourceNode, targetNode) {
|
||||||
|
if (!sourceNode || !targetNode) return false;
|
||||||
|
|
||||||
|
// Check if connection already exists
|
||||||
|
if (this.isConnected(sourceNode, targetNode)) return false;
|
||||||
|
|
||||||
|
// Add new connection
|
||||||
|
this.links.push({
|
||||||
|
source: sourceNode.id,
|
||||||
|
target: targetNode.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update counts
|
||||||
|
this.updateConnectionCounts();
|
||||||
|
|
||||||
|
// Update visualization
|
||||||
|
this.updateVisualization();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lädt gemerkete Knoten aus dem LocalStorage
|
||||||
|
loadBookmarkedNodes() {
|
||||||
|
try {
|
||||||
|
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
||||||
|
return bookmarked ? JSON.parse(bookmarked) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speichert gemerkete Knoten im LocalStorage
|
||||||
|
saveBookmarkedNodes() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('bookmarkedNodes', JSON.stringify(this.bookmarkedNodes));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Speichern der gemerkten Knoten:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüft, ob ein Knoten gemerkt ist
|
||||||
|
isNodeBookmarked(nodeId) {
|
||||||
|
return this.bookmarkedNodes.includes(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merkt einen Knoten oder hebt die Markierung auf
|
||||||
|
toggleBookmark(nodeId) {
|
||||||
|
const index = this.bookmarkedNodes.indexOf(nodeId);
|
||||||
|
if (index === -1) {
|
||||||
|
// Node hinzufügen
|
||||||
|
this.bookmarkedNodes.push(nodeId);
|
||||||
|
this.updateNodeAppearance(nodeId, true);
|
||||||
|
} else {
|
||||||
|
// Node entfernen
|
||||||
|
this.bookmarkedNodes.splice(index, 1);
|
||||||
|
this.updateNodeAppearance(nodeId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Änderungen speichern
|
||||||
|
this.saveBookmarkedNodes();
|
||||||
|
|
||||||
|
// Event auslösen für andere Komponenten
|
||||||
|
const event = new CustomEvent('nodeBookmarkToggled', {
|
||||||
|
detail: {
|
||||||
|
nodeId: nodeId,
|
||||||
|
isBookmarked: index === -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
|
return index === -1; // true wenn jetzt gemerkt, false wenn Markierung aufgehoben
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiert das Aussehen eines Knotens basierend auf Bookmark-Status
|
||||||
|
updateNodeAppearance(nodeId, isBookmarked) {
|
||||||
|
this.g.selectAll('.node-group')
|
||||||
|
.filter(d => d.id === nodeId)
|
||||||
|
.select('.node')
|
||||||
|
.classed('bookmarked', isBookmarked)
|
||||||
|
.attr('stroke', isBookmarked ? '#FFD700' : '#ffffff50')
|
||||||
|
.attr('stroke-width', isBookmarked ? 3 : 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiert das Aussehen aller gemerkten Knoten
|
||||||
|
updateAllBookmarkedNodes() {
|
||||||
|
this.g.selectAll('.node-group')
|
||||||
|
.each((d) => {
|
||||||
|
const isBookmarked = this.isNodeBookmarked(d.id);
|
||||||
|
this.updateNodeAppearance(d.id, isBookmarked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exportiere die Klasse für die Verwendung in anderen Modulen
|
||||||
|
window.MindMapVisualization = MindMapVisualization;
|
||||||
1834
static/mindmap.js
Normal file
1834
static/mindmap.js
Normal file
File diff suppressed because it is too large
Load Diff
88
static/network-animation.js
Normal file
88
static/network-animation.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// Network Animation Effect
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Check if we're on the mindmap page
|
||||||
|
const mindmapContainer = document.getElementById('mindmap-container');
|
||||||
|
if (!mindmapContainer) return;
|
||||||
|
|
||||||
|
// Add enhanced animations for links and nodes
|
||||||
|
setTimeout(function() {
|
||||||
|
// Get all SVG links (connections between nodes)
|
||||||
|
const links = document.querySelectorAll('.link');
|
||||||
|
const nodes = document.querySelectorAll('.node');
|
||||||
|
|
||||||
|
// Add animation to links
|
||||||
|
links.forEach(link => {
|
||||||
|
// Create random animation duration between 15 and 30 seconds
|
||||||
|
const duration = 15 + Math.random() * 15;
|
||||||
|
link.style.animation = `dash ${duration}s linear infinite`;
|
||||||
|
link.style.strokeDasharray = '5, 5';
|
||||||
|
|
||||||
|
// Add pulse effect on hover
|
||||||
|
link.addEventListener('mouseover', function() {
|
||||||
|
this.classList.add('highlighted');
|
||||||
|
this.style.animation = 'dash 5s linear infinite';
|
||||||
|
});
|
||||||
|
|
||||||
|
link.addEventListener('mouseout', function() {
|
||||||
|
this.classList.remove('highlighted');
|
||||||
|
this.style.animation = `dash ${duration}s linear infinite`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add effects to nodes
|
||||||
|
nodes.forEach(node => {
|
||||||
|
node.addEventListener('mouseover', function() {
|
||||||
|
this.querySelector('circle').style.filter = 'drop-shadow(0 0 15px rgba(179, 143, 255, 0.8))';
|
||||||
|
|
||||||
|
// Highlight connected links
|
||||||
|
const nodeId = this.getAttribute('data-id') || this.id;
|
||||||
|
links.forEach(link => {
|
||||||
|
const source = link.getAttribute('data-source');
|
||||||
|
const target = link.getAttribute('data-target');
|
||||||
|
|
||||||
|
if (source === nodeId || target === nodeId) {
|
||||||
|
link.classList.add('highlighted');
|
||||||
|
link.style.animation = 'dash 5s linear infinite';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addEventListener('mouseout', function() {
|
||||||
|
this.querySelector('circle').style.filter = 'drop-shadow(0 0 8px rgba(179, 143, 255, 0.5))';
|
||||||
|
|
||||||
|
// Remove highlight from connected links
|
||||||
|
const nodeId = this.getAttribute('data-id') || this.id;
|
||||||
|
links.forEach(link => {
|
||||||
|
const source = link.getAttribute('data-source');
|
||||||
|
const target = link.getAttribute('data-target');
|
||||||
|
|
||||||
|
if (source === nodeId || target === nodeId) {
|
||||||
|
link.classList.remove('highlighted');
|
||||||
|
const duration = 15 + Math.random() * 15;
|
||||||
|
link.style.animation = `dash ${duration}s linear infinite`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 1000); // Wait for the mindmap to be fully loaded
|
||||||
|
|
||||||
|
// Add network background effect
|
||||||
|
const networkBackground = document.createElement('div');
|
||||||
|
networkBackground.className = 'network-background';
|
||||||
|
networkBackground.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(179, 143, 255, 0.05);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
opacity: 0.15;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: pulse 10s ease-in-out infinite alternate;
|
||||||
|
`;
|
||||||
|
|
||||||
|
mindmapContainer.appendChild(networkBackground);
|
||||||
|
});
|
||||||
232
static/network-background.js
Normal file
232
static/network-background.js
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
// Animated Network Background
|
||||||
|
let canvas, ctx, networkImage;
|
||||||
|
let isImageLoaded = false;
|
||||||
|
let animationSpeed = 0.0003; // Reduzierte Geschwindigkeit für sanftere Rotation
|
||||||
|
let scaleSpeed = 0.0001; // Reduzierte Geschwindigkeit für sanftere Skalierung
|
||||||
|
let opacitySpeed = 0.0002; // Reduzierte Geschwindigkeit für sanftere Opazitätsänderung
|
||||||
|
let rotation = 0;
|
||||||
|
let scale = 1;
|
||||||
|
let opacity = 0.7; // Höhere Basisopazität für bessere Sichtbarkeit
|
||||||
|
let scaleDirection = 1;
|
||||||
|
let opacityDirection = 1;
|
||||||
|
let animationFrameId = null;
|
||||||
|
let isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
let loadAttempts = 0;
|
||||||
|
const MAX_LOAD_ATTEMPTS = 2;
|
||||||
|
|
||||||
|
// Initialize the canvas and load the image
|
||||||
|
function initNetworkBackground() {
|
||||||
|
// Create canvas element if it doesn't exist
|
||||||
|
if (!document.getElementById('network-background')) {
|
||||||
|
canvas = document.createElement('canvas');
|
||||||
|
canvas.id = 'network-background';
|
||||||
|
canvas.style.position = 'fixed';
|
||||||
|
canvas.style.top = '0';
|
||||||
|
canvas.style.left = '0';
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
canvas.style.height = '100%';
|
||||||
|
canvas.style.zIndex = '-5'; // Höher als -10 für den full-page-bg
|
||||||
|
canvas.style.pointerEvents = 'none'; // Stellt sicher, dass der Canvas keine Mausinteraktionen blockiert
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
} else {
|
||||||
|
canvas = document.getElementById('network-background');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas size to window size with pixel ratio consideration
|
||||||
|
resizeCanvas();
|
||||||
|
|
||||||
|
// Get context with alpha enabled
|
||||||
|
ctx = canvas.getContext('2d', { alpha: true });
|
||||||
|
|
||||||
|
// Load the network image - versuche zuerst die SVG-Version
|
||||||
|
networkImage = new Image();
|
||||||
|
networkImage.crossOrigin = "anonymous"; // Vermeidet CORS-Probleme
|
||||||
|
|
||||||
|
// Keine Bilder laden, direkt Fallback-Hintergrund verwenden
|
||||||
|
console.log("Verwende einfachen Hintergrund ohne Bilddateien");
|
||||||
|
isImageLoaded = true; // Animation ohne Hintergrundbild starten
|
||||||
|
startAnimation();
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', debounce(resizeCanvas, 250));
|
||||||
|
|
||||||
|
// Überwache Dark Mode-Änderungen
|
||||||
|
document.addEventListener('darkModeToggled', function(event) {
|
||||||
|
isDarkMode = event.detail.isDark;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion zur Reduzierung der Resize-Event-Aufrufe
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function() {
|
||||||
|
const context = this;
|
||||||
|
const args = arguments;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(function() {
|
||||||
|
func.apply(context, args);
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize canvas to match window size with proper pixel ratio
|
||||||
|
function resizeCanvas() {
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const pixelRatio = window.devicePixelRatio || 1;
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
// Set display size (css pixels)
|
||||||
|
canvas.style.width = width + 'px';
|
||||||
|
canvas.style.height = height + 'px';
|
||||||
|
|
||||||
|
// Set actual size in memory (scaled for pixel ratio)
|
||||||
|
canvas.width = width * pixelRatio;
|
||||||
|
canvas.height = height * pixelRatio;
|
||||||
|
|
||||||
|
// Scale context to match pixel ratio
|
||||||
|
if (ctx) {
|
||||||
|
ctx.scale(pixelRatio, pixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn Animation läuft und Bild geladen, zeichne erneut
|
||||||
|
if (isImageLoaded && animationFrameId) {
|
||||||
|
drawNetworkImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start animation
|
||||||
|
function startAnimation() {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start animation loop
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw network image
|
||||||
|
function drawNetworkImage() {
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// Clear canvas with proper clear method
|
||||||
|
ctx.clearRect(0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1));
|
||||||
|
|
||||||
|
// Save context state
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
// Move to center of canvas
|
||||||
|
ctx.translate(canvas.width / (2 * (window.devicePixelRatio || 1)), canvas.height / (2 * (window.devicePixelRatio || 1)));
|
||||||
|
|
||||||
|
// Rotate
|
||||||
|
ctx.rotate(rotation);
|
||||||
|
|
||||||
|
// Scale
|
||||||
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
|
// Set global opacity, angepasst für Dark Mode
|
||||||
|
ctx.globalAlpha = isDarkMode ? opacity : opacity * 0.8;
|
||||||
|
|
||||||
|
if (isImageLoaded && networkImage.complete) {
|
||||||
|
// Bildgröße berechnen, um den Bildschirm abzudecken
|
||||||
|
const imgAspect = networkImage.width / networkImage.height;
|
||||||
|
const canvasAspect = canvas.width / canvas.height;
|
||||||
|
|
||||||
|
let drawWidth, drawHeight;
|
||||||
|
|
||||||
|
if (canvasAspect > imgAspect) {
|
||||||
|
drawWidth = canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
drawHeight = drawWidth / imgAspect;
|
||||||
|
} else {
|
||||||
|
drawHeight = canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
drawWidth = drawHeight * imgAspect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw image centered
|
||||||
|
ctx.drawImage(
|
||||||
|
networkImage,
|
||||||
|
-drawWidth / 2,
|
||||||
|
-drawHeight / 2,
|
||||||
|
drawWidth,
|
||||||
|
drawHeight
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback: Zeichne einen einfachen Hintergrund mit Punkten
|
||||||
|
drawFallbackBackground();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore context state
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback-Hintergrund mit Punkten und Linien
|
||||||
|
function drawFallbackBackground() {
|
||||||
|
const width = canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const height = canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
// Zeichne einige zufällige Punkte
|
||||||
|
ctx.fillStyle = isDarkMode ? 'rgba(139, 92, 246, 0.2)' : 'rgba(139, 92, 246, 0.1)';
|
||||||
|
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const x = Math.random() * width;
|
||||||
|
const y = Math.random() * height;
|
||||||
|
const radius = Math.random() * 3 + 1;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x - width/2, y - height/2, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
function animate() {
|
||||||
|
// Update animation parameters
|
||||||
|
rotation += animationSpeed;
|
||||||
|
|
||||||
|
// Update scale with oscillation
|
||||||
|
scale += scaleSpeed * scaleDirection;
|
||||||
|
if (scale > 1.05) { // Kleinerer Skalierungsbereich für weniger starke Größenänderung
|
||||||
|
scaleDirection = -1;
|
||||||
|
} else if (scale < 0.95) {
|
||||||
|
scaleDirection = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update opacity with oscillation
|
||||||
|
opacity += opacitySpeed * opacityDirection;
|
||||||
|
if (opacity > 0.75) { // Kleinerer Opazitätsbereich für subtilere Änderungen
|
||||||
|
opacityDirection = -1;
|
||||||
|
} else if (opacity < 0.65) {
|
||||||
|
opacityDirection = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the image
|
||||||
|
drawNetworkImage();
|
||||||
|
|
||||||
|
// Request next frame
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup Funktion für Speicherbereinigung
|
||||||
|
function cleanupNetworkBackground() {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas && canvas.parentNode) {
|
||||||
|
canvas.parentNode.removeChild(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener('resize', resizeCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Führe Initialisierung aus, wenn DOM geladen ist
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initNetworkBackground);
|
||||||
|
} else {
|
||||||
|
initNetworkBackground();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Führe Cleanup durch, wenn das Fenster geschlossen wird
|
||||||
|
window.addEventListener('beforeunload', cleanupNetworkBackground);
|
||||||
100
tailwind.config.js
Normal file
100
tailwind.config.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: 'class',
|
||||||
|
content: [
|
||||||
|
"./templates/**/*.{html,jinja,jinja2}",
|
||||||
|
"./static/**/*.js"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eef5ff',
|
||||||
|
100: '#d9e7ff',
|
||||||
|
200: '#bcd4ff',
|
||||||
|
300: '#8eb8ff',
|
||||||
|
400: '#5a93ff',
|
||||||
|
500: '#2970ff',
|
||||||
|
600: '#1654f6',
|
||||||
|
700: '#1142e2',
|
||||||
|
800: '#1336b7',
|
||||||
|
900: '#153390',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
50: '#f5f2ff',
|
||||||
|
100: '#ece8ff',
|
||||||
|
200: '#ddd5ff',
|
||||||
|
300: '#c4b3ff',
|
||||||
|
400: '#a685ff',
|
||||||
|
500: '#8b55ff',
|
||||||
|
600: '#7833f8',
|
||||||
|
700: '#6924e2',
|
||||||
|
800: '#5720b8',
|
||||||
|
900: '#481c96',
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
50: '#f8f8f9',
|
||||||
|
100: '#e7e7ea',
|
||||||
|
200: '#d1d1d8',
|
||||||
|
300: '#aeaeba',
|
||||||
|
400: '#8a8a99',
|
||||||
|
500: '#6f6f7e',
|
||||||
|
600: '#5b5b69',
|
||||||
|
700: '#49494f',
|
||||||
|
800: '#2c2c33',
|
||||||
|
900: '#18181c',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
'sans': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
|
||||||
|
'mono': ['JetBrains Mono', 'ui-monospace', 'SFMono-Regular', 'monospace']
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
|
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||||
|
'gradient-tech': 'linear-gradient(to right, var(--tw-gradient-stops))',
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
'float': 'float 6s ease-in-out infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
float: {
|
||||||
|
'0%, 100%': { transform: 'translateY(0)' },
|
||||||
|
'50%': { transform: 'translateY(-10px)' },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
DEFAULT: {
|
||||||
|
css: {
|
||||||
|
color: 'rgb(31, 41, 55)',
|
||||||
|
a: {
|
||||||
|
color: 'rgb(41, 112, 255)',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'rgb(22, 84, 246)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
css: {
|
||||||
|
color: 'rgb(229, 231, 235)',
|
||||||
|
a: {
|
||||||
|
color: 'rgb(90, 147, 255)',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'rgb(142, 184, 255)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'soft': '0 4px 15px rgba(0, 0, 0, 0.05)',
|
||||||
|
'glow': '0 0 15px rgba(32, 92, 245, 0.3)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Typography and forms plugins removed, we'll implement their basic functionality in CSS
|
||||||
|
],
|
||||||
|
}
|
||||||
308
templates/admin.html
Normal file
308
templates/admin.html
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Admin-Bereich{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-8 text-gray-800 dark:text-white">Admin-Bereich</h1>
|
||||||
|
|
||||||
|
<!-- Tabs für verschiedene Bereiche -->
|
||||||
|
<div x-data="{ activeTab: 'users' }" class="mb-8">
|
||||||
|
<div class="flex space-x-2 mb-6 overflow-x-auto">
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'users'"
|
||||||
|
:class="activeTab === 'users' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||||
|
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||||
|
<i class="fas fa-users mr-2"></i> Benutzer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'nodes'"
|
||||||
|
:class="activeTab === 'nodes' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||||
|
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||||
|
<i class="fas fa-project-diagram mr-2"></i> Mindmap-Knoten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'thoughts'"
|
||||||
|
:class="activeTab === 'thoughts' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||||
|
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||||
|
<i class="fas fa-lightbulb mr-2"></i> Gedanken
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="activeTab = 'stats'"
|
||||||
|
:class="activeTab === 'stats' ? 'bg-primary-600 text-white' : 'bg-white/10 text-gray-700 dark:text-gray-200'"
|
||||||
|
class="px-4 py-2 rounded-lg font-medium transition-all">
|
||||||
|
<i class="fas fa-chart-bar mr-2"></i> Statistiken
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Benutzer-Tab -->
|
||||||
|
<div x-show="activeTab === 'users'" class="glass-morphism rounded-lg p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Benutzerverwaltung</h2>
|
||||||
|
<button class="btn-outline">
|
||||||
|
<i class="fas fa-plus mr-2"></i> Neuer Benutzer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Benutzername</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">E-Mail</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Admin</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.id }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ user.username }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.email }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||||
|
{% if user.is_admin %}
|
||||||
|
<span class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 px-2 py-1 rounded text-xs">Admin</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 px-2 py-1 rounded text-xs">User</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ user.thoughts|length }}</td>
|
||||||
|
<td class="px-4 py-3 flex space-x-2">
|
||||||
|
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mindmap-Knoten-Tab -->
|
||||||
|
<div x-show="activeTab === 'nodes'" class="glass-morphism rounded-lg p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Mindmap-Knoten Verwaltung</h2>
|
||||||
|
<button class="btn-outline">
|
||||||
|
<i class="fas fa-plus mr-2"></i> Neuer Knoten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Name</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Elternknoten</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Gedanken</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for node in nodes %}
|
||||||
|
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.id }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ node.name }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||||
|
{% if node.parent %}
|
||||||
|
{{ node.parent.name }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-400 dark:text-gray-500">Wurzelknoten</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ node.thoughts|length }}</td>
|
||||||
|
<td class="px-4 py-3 flex space-x-2">
|
||||||
|
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gedanken-Tab -->
|
||||||
|
<div x-show="activeTab === 'thoughts'" class="glass-morphism rounded-lg p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white">Gedanken-Verwaltung</h2>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" placeholder="Suchen..." class="form-input pl-10 pr-4 py-2 rounded-lg bg-white/10 border border-gray-200/20 dark:border-gray-700/20 focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700 dark:text-gray-200">
|
||||||
|
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-outline">
|
||||||
|
<i class="fas fa-filter mr-2"></i> Filter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">ID</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Titel</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Autor</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Datum</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Bewertung</th>
|
||||||
|
<th class="px-4 py-2 text-gray-700 dark:text-gray-300">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for thought in thoughts %}
|
||||||
|
<tr class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-dark-700/30">
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.id }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300 font-medium">{{ thought.title }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.author.username }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">{{ thought.timestamp.strftime('%d.%m.%Y') }}</td>
|
||||||
|
<td class="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-2">{{ "%.1f"|format(thought.average_rating) }}</span>
|
||||||
|
<div class="flex">
|
||||||
|
{% for i in range(5) %}
|
||||||
|
{% if i < thought.average_rating|int %}
|
||||||
|
<i class="fas fa-star text-yellow-400"></i>
|
||||||
|
{% elif i < (thought.average_rating|int + 0.5) %}
|
||||||
|
<i class="fas fa-star-half-alt text-yellow-400"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="far fa-star text-yellow-400"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 flex space-x-2">
|
||||||
|
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiken-Tab -->
|
||||||
|
<div x-show="activeTab === 'stats'" class="glass-morphism rounded-lg p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-6 text-gray-800 dark:text-white">Systemstatistiken</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div class="glass-effect p-4 rounded-lg">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class="bg-blue-500/20 p-3 rounded-lg mr-3">
|
||||||
|
<i class="fas fa-users text-blue-500"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Benutzer</h3>
|
||||||
|
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ users|length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect p-4 rounded-lg">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class="bg-purple-500/20 p-3 rounded-lg mr-3">
|
||||||
|
<i class="fas fa-project-diagram text-purple-500"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Knoten</h3>
|
||||||
|
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ nodes|length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect p-4 rounded-lg">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class="bg-green-500/20 p-3 rounded-lg mr-3">
|
||||||
|
<i class="fas fa-lightbulb text-green-500"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Gedanken</h3>
|
||||||
|
<p class="text-2xl font-bold text-gray-800 dark:text-white">{{ thoughts|length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect p-4 rounded-lg">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<div class="bg-red-500/20 p-3 rounded-lg mr-3">
|
||||||
|
<i class="fas fa-comments text-red-500"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300">Kommentare</h3>
|
||||||
|
<p class="text-2xl font-bold text-gray-800 dark:text-white">
|
||||||
|
{% set comment_count = 0 %}
|
||||||
|
{% for thought in thoughts %}
|
||||||
|
{% set comment_count = comment_count + thought.comments|length %}
|
||||||
|
{% endfor %}
|
||||||
|
{{ comment_count }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="glass-effect p-4 rounded-lg">
|
||||||
|
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Aktive Benutzer</h3>
|
||||||
|
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Hier würde ein Aktivitätsdiagramm angezeigt werden</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect p-4 rounded-lg">
|
||||||
|
<h3 class="text-lg font-bold mb-4 text-gray-800 dark:text-white">Gedanken pro Kategorie</h3>
|
||||||
|
<div class="h-64 flex items-center justify-center bg-gray-100/20 dark:bg-dark-700/20 rounded">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">Hier würde eine Verteilungsstatistik angezeigt werden</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System-Log (immer sichtbar) -->
|
||||||
|
<div class="mt-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4 text-gray-800 dark:text-white">System-Log</h2>
|
||||||
|
<div class="glass-morphism rounded-lg p-4 h-32 overflow-y-auto font-mono text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<div class="text-green-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] System gestartet</div>
|
||||||
|
<div class="text-blue-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Admin-Bereich aufgerufen von {{ current_user.username }}</div>
|
||||||
|
<div class="text-yellow-500">[WARN] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Hohe Serverauslastung erkannt</div>
|
||||||
|
<div class="text-gray-500">[INFO] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] Backup erfolgreich erstellt</div>
|
||||||
|
<div class="text-red-500">[ERROR] [{{ now.strftime('%Y-%m-%d %H:%M:%S') }}] API-Zugriffsfehler (Timeout) bei externer Anfrage</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
// Admin-spezifische JavaScript-Funktionen
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Admin-Bereich geladen');
|
||||||
|
|
||||||
|
// Beispiel für AJAX-Ladeverhalten von Daten
|
||||||
|
// Kann später durch echte API-Calls ersetzt werden
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
71
templates/agb.html
Normal file
71
templates/agb.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}AGB{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<div class="card p-6 md:p-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6 gradient-text">Allgemeine Geschäftsbedingungen</h1>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">1. Geltungsbereich</h2>
|
||||||
|
<p class="mb-4">Diese Allgemeinen Geschäftsbedingungen (nachfolgend "AGB") gelten für die Nutzung der MindMap-Plattform (nachfolgend "Plattform"), die von der MindMap GmbH, Musterstraße 123, 12345 Musterstadt (nachfolgend "Anbieter") betrieben wird.</p>
|
||||||
|
<p class="mb-4">Mit der Registrierung und/oder Nutzung der Plattform erkennt der Nutzer diese AGB an. Die Nutzung der Plattform ist nur zulässig, wenn der Nutzer diese AGB akzeptiert.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">2. Leistungsbeschreibung</h2>
|
||||||
|
<p class="mb-4">Die Plattform bietet dem Nutzer die Möglichkeit, komplexe Informationen in Form von Mindmaps zu visualisieren, zu organisieren und zu teilen. Der genaue Funktionsumfang ergibt sich aus der jeweiligen Leistungsbeschreibung auf der Website des Anbieters.</p>
|
||||||
|
<p class="mb-4">Der Anbieter ist berechtigt, die angebotenen Dienste zu ändern, neue Dienste unentgeltlich oder entgeltlich verfügbar zu machen und die Bereitstellung unentgeltlicher Dienste einzustellen. Der Anbieter wird hierbei jeweils auf die berechtigten Interessen des Nutzers Rücksicht nehmen.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">3. Registrierung und Nutzerkonto</h2>
|
||||||
|
<p class="mb-4">Die Nutzung bestimmter Funktionen der Plattform setzt die Registrierung eines Nutzerkontos voraus. Die Registrierung ist nur volljährigen und voll geschäftsfähigen natürlichen Personen erlaubt.</p>
|
||||||
|
<p class="mb-4">Der Nutzer verpflichtet sich, bei der Registrierung wahrheitsgemäße und vollständige Angaben zu machen und diese Daten stets aktuell zu halten. Es ist nicht gestattet, mehrere Nutzerkonten zu erstellen.</p>
|
||||||
|
<p class="mb-4">Der Nutzer ist verpflichtet, seine Zugangsdaten geheim zu halten und vor dem Zugriff durch unbefugte Dritte zu schützen. Der Anbieter wird den Nutzer niemals nach seinem Passwort fragen.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">4. Nutzungsrechte</h2>
|
||||||
|
<p class="mb-4">Der Anbieter gewährt dem Nutzer für die Dauer der Vertragslaufzeit ein einfaches, nicht übertragbares Recht zur Nutzung der Plattform im vertraglich vereinbarten Umfang.</p>
|
||||||
|
<p class="mb-4">Der Nutzer räumt dem Anbieter an den von ihm auf der Plattform eingestellten Inhalten ein einfaches, übertragbares, unterlizenzierbares, räumlich und zeitlich unbeschränktes Nutzungsrecht ein, soweit dies für den Betrieb der Plattform erforderlich ist.</p>
|
||||||
|
<p class="mb-4">Der Nutzer garantiert, dass er über alle Rechte an den von ihm eingestellten Inhalten verfügt und durch diese keine Rechte Dritter verletzt werden.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">5. Pflichten des Nutzers</h2>
|
||||||
|
<p class="mb-4">Der Nutzer verpflichtet sich, die Plattform nur im Einklang mit diesen AGB und den geltenden Gesetzen zu nutzen. Insbesondere ist es dem Nutzer untersagt:</p>
|
||||||
|
<ul class="list-disc pl-6 mb-4 space-y-2">
|
||||||
|
<li>die Plattform für rechtswidrige oder betrügerische Zwecke zu nutzen</li>
|
||||||
|
<li>rechtswidrige, beleidigende, diskriminierende oder anderweitig anstößige Inhalte zu verbreiten</li>
|
||||||
|
<li>Schadsoftware, Viren oder andere schädliche Computercodes zu verbreiten</li>
|
||||||
|
<li>die normale Funktion der Plattform zu stören oder übermäßig zu belasten</li>
|
||||||
|
<li>auf die Plattform mit automatisierten Mitteln zuzugreifen (wie z.B. Bots, Scraper)</li>
|
||||||
|
<li>die Plattform zu reverse-engineeren, zu dekompilieren oder zu disassemblieren</li>
|
||||||
|
</ul>
|
||||||
|
<p class="mb-4">Der Anbieter behält sich das Recht vor, bei Verstößen gegen diese Pflichten entsprechende Maßnahmen zu ergreifen, einschließlich der Sperrung des Nutzerkontos.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">6. Verfügbarkeit und Wartung</h2>
|
||||||
|
<p class="mb-4">Der Anbieter ist bemüht, eine hohe Verfügbarkeit der Plattform zu gewährleisten, kann jedoch keine unterbrechungsfreie Verfügbarkeit garantieren. Insbesondere können Wartungsarbeiten, Sicherheits- oder Kapazitätsprobleme sowie Ereignisse, die außerhalb des Einflussbereichs des Anbieters liegen, zu vorübergehenden Unterbrechungen führen.</p>
|
||||||
|
<p class="mb-4">Der Anbieter wird planmäßige Wartungsarbeiten, sofern möglich, vorher ankündigen und zu Zeiten durchführen, in denen die Nutzung der Plattform typischerweise gering ist.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">7. Haftung</h2>
|
||||||
|
<p class="mb-4">Der Anbieter haftet unbeschränkt für Vorsatz und grobe Fahrlässigkeit sowie nach dem Produkthaftungsgesetz. Für leichte Fahrlässigkeit haftet der Anbieter nur bei Verletzung einer wesentlichen Vertragspflicht und der Höhe nach beschränkt auf die bei Vertragsschluss vorhersehbaren und vertragstypischen Schäden. Wesentliche Vertragspflichten sind solche, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Nutzer regelmäßig vertrauen darf.</p>
|
||||||
|
<p class="mb-4">Diese Haftungsbeschränkung gilt nicht für Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">8. Schlussbestimmungen</h2>
|
||||||
|
<p class="mb-4">Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.</p>
|
||||||
|
<p class="mb-4">Sollten einzelne Bestimmungen dieser AGB unwirksam sein oder werden, bleibt die Wirksamkeit der übrigen Bestimmungen unberührt.</p>
|
||||||
|
<p class="mb-4">Der Anbieter behält sich vor, diese AGB jederzeit zu ändern. Änderungen werden dem Nutzer rechtzeitig vor ihrem Inkrafttreten mitgeteilt. Die Änderungen gelten als akzeptiert, wenn der Nutzer ihnen nicht innerhalb von vier Wochen nach Erhalt der Mitteilung widerspricht.</p>
|
||||||
|
<p class="mb-4">Stand: Mai 2023</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
535
templates/base.html
Normal file
535
templates/base.html
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Systades - {% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.svg') }}" type="image/svg+xml">
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
||||||
|
|
||||||
|
<!-- Meta Tags -->
|
||||||
|
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||||
|
<meta name="keywords" content="systades, wissen, visualisierung, lernen, gedanken, theorie">
|
||||||
|
<meta name="author" content="Systades-Team">
|
||||||
|
|
||||||
|
<!-- Tailwind CSS über CDN -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'ui-monospace', 'monospace']
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f5f3ff',
|
||||||
|
100: '#ede9fe',
|
||||||
|
200: '#ddd6fe',
|
||||||
|
300: '#c4b5fd',
|
||||||
|
400: '#a78bfa',
|
||||||
|
500: '#8b5cf6',
|
||||||
|
600: '#7c3aed',
|
||||||
|
700: '#6d28d9',
|
||||||
|
800: '#5b21b6',
|
||||||
|
900: '#4c1d95'
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
50: '#ecfdf5',
|
||||||
|
100: '#d1fae5',
|
||||||
|
200: '#a7f3d0',
|
||||||
|
300: '#6ee7b7',
|
||||||
|
400: '#34d399',
|
||||||
|
500: '#10b981',
|
||||||
|
600: '#059669',
|
||||||
|
700: '#047857',
|
||||||
|
800: '#065f46',
|
||||||
|
900: '#064e3b'
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
500: '#374151',
|
||||||
|
600: '#1f2937',
|
||||||
|
700: '#111827',
|
||||||
|
800: '#0e1220',
|
||||||
|
900: '#0a0e19'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Assistent CSS -->
|
||||||
|
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/src/cybernetwork-bg.css') }}">
|
||||||
|
|
||||||
|
<!-- Basis-Stylesheet -->
|
||||||
|
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Base-Styles ausgelagert in eigene Datei -->
|
||||||
|
<link href="{{ url_for('static', filename='css/base-styles.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Alpine.js -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.3/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Network Background Script -->
|
||||||
|
<script src="{{ url_for('static', filename='network-background.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Hauptmodul laden (als ES6 Modul) -->
|
||||||
|
<script type="module">
|
||||||
|
import MindMap from "{{ url_for('static', filename='js/main.js') }}";
|
||||||
|
// Alpine.js-Integration
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.data('layout', () => ({
|
||||||
|
darkMode: false,
|
||||||
|
mobileMenuOpen: false,
|
||||||
|
userMenuOpen: false,
|
||||||
|
showSettingsModal: false,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchDarkModeFromSession();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchDarkModeFromSession() {
|
||||||
|
// Lade den Dark Mode-Status vom Server
|
||||||
|
fetch('/get_dark_mode')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.darkMode = data.darkMode === 'true';
|
||||||
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Dark Mode-Einstellung:', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleDarkMode() {
|
||||||
|
this.darkMode = !this.darkMode;
|
||||||
|
document.querySelector('html').classList.toggle('dark', this.darkMode);
|
||||||
|
|
||||||
|
// Speichere den Dark Mode-Status auf dem Server
|
||||||
|
fetch('/set_dark_mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ darkMode: this.darkMode })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Zusätzlich im localStorage speichern für sofortige Reaktion bei Seitenwechsel
|
||||||
|
localStorage.setItem('darkMode', this.darkMode ? 'dark' : 'light');
|
||||||
|
// Event auslösen für andere Komponenten
|
||||||
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
|
detail: { isDark: this.darkMode }
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// MindMap global verfügbar machen (für Alpine.js und andere nicht-Module Skripte)
|
||||||
|
window.MindMap = MindMap;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Seitenspezifische Styles -->
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- Cybertechnisches Netzwerk-Hintergrund -->
|
||||||
|
<script type="module" src="{{ url_for('static', filename='js/modules/cyber-network-init.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden">
|
||||||
|
<!-- Cybertechnisches Netzwerk-Hintergrund Container (wird via JavaScript befüllt) -->
|
||||||
|
<div id="cyber-background-container" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; overflow: hidden;"></div>
|
||||||
|
|
||||||
|
<!-- Globaler Hintergrund -->
|
||||||
|
<div class="full-page-bg"></div>
|
||||||
|
<!-- Statischer Fallback-Hintergrund (wird nur angezeigt, wenn JavaScript deaktiviert ist) -->
|
||||||
|
<div class="fixed inset-0 z-[-9] bg-cover bg-center opacity-50"></div>
|
||||||
|
|
||||||
|
<!-- App-Container -->
|
||||||
|
<div id="app-container" class="flex flex-col min-h-screen" x-data="layout">
|
||||||
|
<!-- Hauptnavigation -->
|
||||||
|
<nav class="sticky top-0 left-0 right-0 z-50 transition-all duration-300 py-4 px-5 border-b glass-morphism"
|
||||||
|
x-bind:class="darkMode ? 'glass-navbar-dark border-white/10' : 'glass-navbar-light border-gray-200/50'">
|
||||||
|
<div class="container mx-auto flex justify-between items-center">
|
||||||
|
<!-- Logo -->
|
||||||
|
<a href="{{ url_for('index') }}" class="flex items-center group">
|
||||||
|
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Hauptnavigation - Desktop -->
|
||||||
|
<div class="hidden md:flex items-center space-x-5">
|
||||||
|
<a href="{{ url_for('index') }}"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'nav-link-active' if request.endpoint == 'index' else '' }}'
|
||||||
|
: '{{ 'nav-link-light-active' if request.endpoint == 'index' else 'nav-link-light' }}'">
|
||||||
|
<i class="fa-solid fa-home mr-2"></i>Start
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'nav-link-active' if request.endpoint == 'mindmap' else '' }}'
|
||||||
|
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
||||||
|
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'nav-link-active' if request.endpoint == 'search_thoughts_page' else '' }}'
|
||||||
|
: '{{ 'nav-link-light-active' if request.endpoint == 'search_thoughts_page' else 'nav-link-light' }}'">
|
||||||
|
<i class="fa-solid fa-search mr-2"></i>Suche
|
||||||
|
</a>
|
||||||
|
<!-- KI-Assistent Button -->
|
||||||
|
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true)"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gradient-to-r from-purple-600/80 to-blue-500/80 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5'
|
||||||
|
: 'bg-gradient-to-r from-purple-500/20 to-blue-400/20 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300 hover:-translate-y-0.5'">
|
||||||
|
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||||
|
</button>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('profile') }}"
|
||||||
|
class="nav-link flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'nav-link-active' if request.endpoint == 'profile' else '' }}'
|
||||||
|
: '{{ 'nav-link-light-active' if request.endpoint == 'profile' else 'nav-link-light' }}'">
|
||||||
|
<i class="fa-solid fa-user mr-2"></i>Profil
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechte Seite -->
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<!-- Dark Mode Toggle Switch -->
|
||||||
|
<div class="flex items-center cursor-pointer" @click="toggleDarkMode">
|
||||||
|
<div class="relative w-12 h-6">
|
||||||
|
<input type="checkbox" id="darkModeToggle" class="sr-only" x-model="darkMode">
|
||||||
|
<div class="block w-12 h-6 rounded-full transition-colors duration-300"
|
||||||
|
x-bind:class="darkMode ? 'bg-blue-400/50' : 'bg-gray-400/50'"></div>
|
||||||
|
<div class="dot absolute left-1 top-1 w-4 h-4 rounded-full transition-transform duration-300 shadow-md"
|
||||||
|
x-bind:class="darkMode ? 'bg-blue-500 transform translate-x-6' : 'bg-white'"></div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 hidden sm:block"
|
||||||
|
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||||
|
<span x-text="darkMode ? 'Dunkel' : 'Hell'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 sm:hidden"
|
||||||
|
x-bind:class="darkMode ? 'text-white/90' : 'text-gray-700'">
|
||||||
|
<i class="fa-solid" :class="darkMode ? 'fa-sun' : 'fa-moon'"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profil-Link oder Login -->
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<div class="relative" x-data="{ open: false }">
|
||||||
|
<button @click="open = !open"
|
||||||
|
class="flex items-center space-x-2 p-2 rounded-full transition-all duration-300 cursor-pointer"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-800/80 text-white/90 hover:bg-gray-700/80'
|
||||||
|
: 'bg-gray-200/80 text-gray-700 hover:bg-gray-300/80'">
|
||||||
|
<div class="w-9 h-9 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden"
|
||||||
|
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
|
||||||
|
{% if current_user.avatar %}
|
||||||
|
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||||
|
{% else %}
|
||||||
|
{{ current_user.username[0].upper() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
||||||
|
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
||||||
|
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Dropdown-Menü -->
|
||||||
|
<div x-show="open"
|
||||||
|
@click.away="open = false"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="absolute right-0 mt-2 w-52 rounded-2xl overflow-hidden shadow-lg transform origin-top-right z-50"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-800/95 backdrop-blur-md border border-white/10'
|
||||||
|
: 'bg-white/95 backdrop-blur-md border border-gray-200/50'">
|
||||||
|
<a href="{{ url_for('profile') }}"
|
||||||
|
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'text-white/90 hover:bg-purple-500/20'
|
||||||
|
: 'text-gray-700 hover:bg-purple-500/10'">
|
||||||
|
<i class="fa-solid fa-user mr-2 text-purple-400"></i>Profil
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('my_account') }}"
|
||||||
|
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'text-white/90 hover:bg-purple-500/20'
|
||||||
|
: 'text-gray-700 hover:bg-purple-500/10'">
|
||||||
|
<i class="fa-solid fa-bookmark mr-2 text-purple-400"></i>Meine Merkliste
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('settings') }}"
|
||||||
|
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'text-white/90 hover:bg-purple-500/20'
|
||||||
|
: 'text-gray-700 hover:bg-purple-500/10'">
|
||||||
|
<i class="fa-solid fa-gear mr-2 text-purple-400"></i>Einstellungen
|
||||||
|
</a>
|
||||||
|
<div class="my-2 h-px" x-bind:class="darkMode ? 'bg-white/10' : 'bg-gray-200'"></div>
|
||||||
|
<a href="{{ url_for('logout') }}"
|
||||||
|
class="block px-4 py-3 transition-colors duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'text-white/90 hover:bg-red-500/20'
|
||||||
|
: 'text-gray-700 hover:bg-red-500/10'">
|
||||||
|
<i class="fa-solid fa-right-from-bracket mr-2 text-red-400"></i>Abmelden
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('login') }}"
|
||||||
|
class="flex items-center px-4 py-2.5 rounded-xl font-medium transition-all duration-300"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-800/80 text-white hover:bg-gray-700/80 shadow-md hover:shadow-lg hover:-translate-y-0.5'
|
||||||
|
: 'bg-gray-200/80 text-gray-800 hover:bg-gray-300/80 shadow-sm hover:shadow-md hover:-translate-y-0.5'">
|
||||||
|
<i class="fa-solid fa-user mr-2"></i>Mein Konto
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Mobilmenü-Button -->
|
||||||
|
<button @click="mobileMenuOpen = !mobileMenuOpen"
|
||||||
|
class="md:hidden rounded-xl p-2.5 transition-colors duration-200 focus:outline-none"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'text-white/90 hover:bg-gray-700/50'
|
||||||
|
: 'text-gray-700 hover:bg-gray-200/80'">
|
||||||
|
<i class="fa-solid" :class="mobileMenuOpen ? 'fa-times' : 'fa-bars'"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile Menü -->
|
||||||
|
<div x-show="mobileMenuOpen"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0 -translate-y-4"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave-end="opacity-0 -translate-y-4"
|
||||||
|
class="md:hidden w-full z-40 border-b"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gray-900/90 backdrop-blur-lg border-white/10'
|
||||||
|
: 'bg-white/90 backdrop-blur-lg border-gray-200'">
|
||||||
|
<div class="px-4 py-4 space-y-3">
|
||||||
|
<a href="{{ url_for('index') }}"
|
||||||
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'index' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||||
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'index' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
|
<i class="fa-solid fa-home w-5 mr-3"></i>Start
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('mindmap') }}"
|
||||||
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'mindmap' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||||
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
|
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'search_thoughts_page' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||||
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'search_thoughts_page' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
|
<i class="fa-solid fa-search w-5 mr-3"></i>Suche
|
||||||
|
</a>
|
||||||
|
<!-- KI-Button für Mobilmenü -->
|
||||||
|
<button onclick="window.MindMap && window.MindMap.assistant && window.MindMap.assistant.toggleAssistant(true); mobileMenuOpen = false;"
|
||||||
|
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? 'bg-gradient-to-r from-purple-600/30 to-blue-500/30 text-white hover:from-purple-600/40 hover:to-blue-500/40'
|
||||||
|
: 'bg-gradient-to-r from-purple-500/10 to-blue-400/10 text-gray-900 hover:from-purple-500/20 hover:to-blue-400/20'">
|
||||||
|
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||||
|
</button>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('profile') }}"
|
||||||
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
|
x-bind:class="darkMode
|
||||||
|
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'profile' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||||
|
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'profile' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||||
|
<i class="fa-solid fa-user w-5 mr-3"></i>Profil
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hauptinhalt -->
|
||||||
|
<main class="flex-grow pt-6">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="mt-12 py-10 transition-colors duration-300 rounded-t-3xl mx-4 sm:mx-6 md:mx-8"
|
||||||
|
:class="darkMode ? 'bg-gray-900/60 backdrop-blur-xl border-t border-white/10' : 'bg-white/60 backdrop-blur-xl border-t border-gray-200/50'">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||||
|
<!-- Logo und Beschreibung -->
|
||||||
|
<div class="text-center md:text-left flex flex-col">
|
||||||
|
<a href="{{ url_for('index') }}" class="text-2xl font-bold mb-4 gradient-text inline-block transform transition-transform hover:scale-105">Systades</a>
|
||||||
|
<p class="mt-2 text-sm max-w-md"
|
||||||
|
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen und Gedanken in einem strukturierten Format.
|
||||||
|
</p>
|
||||||
|
<!-- Social Media Icons -->
|
||||||
|
<div class="flex items-center space-x-4 mt-6 justify-center md:justify-start">
|
||||||
|
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||||
|
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||||
|
<i class="fab fa-twitter text-xl"></i>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||||
|
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||||
|
<i class="fab fa-linkedin text-xl"></i>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||||
|
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||||
|
<i class="fab fa-github text-xl"></i>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="transition-all duration-200 transform hover:scale-110 hover:-translate-y-1"
|
||||||
|
:class="darkMode ? 'text-purple-400 hover:text-purple-300' : 'text-purple-600 hover:text-purple-500'">
|
||||||
|
<i class="fab fa-discord text-xl"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<div class="grid grid-cols-2 gap-8">
|
||||||
|
<div class="flex flex-col space-y-3">
|
||||||
|
<h3 class="font-semibold text-lg mb-2"
|
||||||
|
:class="darkMode ? 'text-white' : 'text-gray-800'">Navigation</h3>
|
||||||
|
<a href="{{ url_for('index') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Startseite
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Mindmap
|
||||||
|
</a>
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Profil
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('my_account') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Meine Merkliste
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('login') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Anmelden
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('register') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Registrieren
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-3">
|
||||||
|
<h3 class="font-semibold text-lg mb-2"
|
||||||
|
:class="darkMode ? 'text-white' : 'text-gray-800'">Rechtliches</h3>
|
||||||
|
<a href="{{ url_for('impressum') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Impressum
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('datenschutz') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
Datenschutz
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('agb') }}" class="text-sm transition-all duration-200"
|
||||||
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
|
AGB
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Newsletter Anmeldung -->
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h3 class="font-semibold text-lg mb-4"
|
||||||
|
:class="darkMode ? 'text-white' : 'text-gray-800'">Newsletter</h3>
|
||||||
|
<p class="text-sm mb-4"
|
||||||
|
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
|
||||||
|
Bleibe auf dem Laufenden mit unseren neuesten Funktionen und Updates.
|
||||||
|
</p>
|
||||||
|
<form class="flex flex-col space-y-3">
|
||||||
|
<input type="email" placeholder="Deine E-Mail Adresse"
|
||||||
|
class="px-4 py-2.5 rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
:class="darkMode ? 'bg-gray-800/80 text-white border border-gray-700 focus:bg-gray-800' : 'bg-white/80 text-gray-800 border border-gray-300 focus:bg-white'" />
|
||||||
|
<button type="submit"
|
||||||
|
class="px-4 py-2.5 rounded-xl font-medium transition-all duration-300 bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-md hover:shadow-lg hover:-translate-y-0.5">
|
||||||
|
Abonnieren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Untere Linie -->
|
||||||
|
<div class="mt-10 pt-6 border-t flex flex-col md:flex-row justify-between items-center"
|
||||||
|
:class="darkMode ? 'border-gray-800/50 text-gray-400' : 'border-gray-300/50 text-gray-600'">
|
||||||
|
<div class="text-xs md:text-sm mb-3 md:mb-0">
|
||||||
|
© {{ current_year }} Systades. Alle Rechte vorbehalten.
|
||||||
|
</div>
|
||||||
|
<div class="text-xs md:text-sm">
|
||||||
|
Designed with <i class="fas fa-heart text-pink-500"></i> in Deutschland
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hilfsscripts -->
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- KI-Chat Initialisierung -->
|
||||||
|
<script type="module">
|
||||||
|
// Importiere und initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
||||||
|
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
||||||
|
import ChatGPTAssistant from "{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}";
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
||||||
|
if (!window.MindMap || !window.MindMap.assistant) {
|
||||||
|
console.log('KI-Assistent wird direkt initialisiert...');
|
||||||
|
const assistant = new ChatGPTAssistant();
|
||||||
|
assistant.init();
|
||||||
|
|
||||||
|
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
||||||
|
if (!window.MindMap) {
|
||||||
|
window.MindMap = {};
|
||||||
|
}
|
||||||
|
window.MindMap.assistant = assistant;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
templates/datenschutz.html
Normal file
64
templates/datenschutz.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Datenschutz{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<div class="card p-6 md:p-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6 gradient-text">Datenschutzerklärung</h1>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">1. Datenschutz auf einen Blick</h2>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-2">Allgemeine Hinweise</h3>
|
||||||
|
<p class="mb-4">Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können. Ausführliche Informationen zum Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgeführten Datenschutzerklärung.</p>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-2">Datenerfassung auf dieser Website</h3>
|
||||||
|
<p class="mb-4"><strong>Wer ist verantwortlich für die Datenerfassung auf dieser Website?</strong></p>
|
||||||
|
<p class="mb-4">Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten können Sie dem Impressum dieser Website entnehmen.</p>
|
||||||
|
|
||||||
|
<p class="mb-4"><strong>Wie erfassen wir Ihre Daten?</strong></p>
|
||||||
|
<p class="mb-4">Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z. B. um Daten handeln, die Sie in ein Kontaktformular eingeben.</p>
|
||||||
|
<p class="mb-4">Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z. B. Internetbrowser, Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese Website betreten.</p>
|
||||||
|
|
||||||
|
<p class="mb-4"><strong>Wofür nutzen wir Ihre Daten?</strong></p>
|
||||||
|
<p class="mb-4">Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.</p>
|
||||||
|
|
||||||
|
<p class="mb-4"><strong>Welche Rechte haben Sie bezüglich Ihrer Daten?</strong></p>
|
||||||
|
<p class="mb-4">Sie haben jederzeit das Recht, unentgeltlich Auskunft über Herkunft, Empfänger und Zweck Ihrer gespeicherten personenbezogenen Daten zu erhalten. Sie haben außerdem ein Recht, die Berichtigung oder Löschung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben, können Sie diese Einwilligung jederzeit für die Zukunft widerrufen. Außerdem haben Sie das Recht, unter bestimmten Umständen die Einschränkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">2. Allgemeine Hinweise und Pflichtinformationen</h2>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-2">Datenschutz</h3>
|
||||||
|
<p class="mb-4">Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.</p>
|
||||||
|
<p class="mb-4">Wenn Sie diese Website benutzen, werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie persönlich identifiziert werden können. Die vorliegende Datenschutzerklärung erläutert, welche Daten wir erheben und wofür wir sie nutzen. Sie erläutert auch, wie und zu welchem Zweck das geschieht.</p>
|
||||||
|
<p class="mb-4">Wir weisen darauf hin, dass die Datenübertragung im Internet (z. B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich.</p>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-2">Hinweis zur verantwortlichen Stelle</h3>
|
||||||
|
<p class="mb-4">Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:</p>
|
||||||
|
<p class="mb-4">
|
||||||
|
MindMap GmbH<br>
|
||||||
|
Musterstraße 123<br>
|
||||||
|
12345 Musterstadt<br>
|
||||||
|
Deutschland
|
||||||
|
</p>
|
||||||
|
<p class="mb-4">
|
||||||
|
Telefon: +49 (0) 123 456789<br>
|
||||||
|
E-Mail: info@mindmap-example.com
|
||||||
|
</p>
|
||||||
|
<p class="mb-4">Verantwortliche Stelle ist die natürliche oder juristische Person, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z. B. Namen, E-Mail-Adressen o. Ä.) entscheidet.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">3. Datenerfassung auf dieser Website</h2>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-2">Cookies</h3>
|
||||||
|
<p class="mb-4">Unsere Internetseiten verwenden so genannte "Cookies". Cookies sind kleine Datenpakete und richten auf Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung (Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.</p>
|
||||||
|
<p class="mb-4">Cookies können von uns (First-Party-Cookies) oder von Drittunternehmen stammen (sog. Third-Party-Cookies). Third-Party-Cookies ermöglichen die Einbindung bestimmter Dienstleistungen von Drittunternehmen innerhalb von Webseiten (z. B. Cookies zur Abwicklung von Zahlungsdienstleistungen).</p>
|
||||||
|
<p class="mb-4">Die meisten Browser bieten Ihnen die Möglichkeit, das Setzen von Cookies für bestimmte Webseiten zu verbieten oder Cookies jedes Mal vor dem Akzeptieren anzuzeigen. Ebenso können Sie jederzeit mitgeteilt bekommen, sobald Ihr Browser ein neues Cookie empfängt.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
23
templates/errors/403.html
Normal file
23
templates/errors/403.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}403 - Zugriff verweigert{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">403</h1>
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Zugriff verweigert</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben nicht die erforderlichen Berechtigungen, um auf diese Seite zuzugreifen. Bitte melden Sie sich an oder nutzen Sie ein Konto mit entsprechenden Rechten.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href="{{ url_for('index') }}" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="javascript:history.back()" class="btn-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
23
templates/errors/404.html
Normal file
23
templates/errors/404.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">404</h1>
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Seite nicht gefunden</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8">Die gesuchte Seite existiert nicht oder wurde verschoben. Bitte prüfen Sie die URL oder nutzen Sie die Navigation.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href="{{ url_for('index') }}" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="javascript:history.back()" class="btn-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
23
templates/errors/429.html
Normal file
23
templates/errors/429.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}429 - Zu viele Anfragen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">429</h1>
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Zu viele Anfragen</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8">Sie haben zu viele Anfragen in kurzer Zeit gestellt. Bitte warten Sie einen Moment und versuchen Sie es dann erneut.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href="{{ url_for('index') }}" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="javascript:history.back()" class="btn-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
23
templates/errors/500.html
Normal file
23
templates/errors/500.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}500 - Serverfehler{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-[65vh] flex flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div class="glass-effect max-w-2xl w-full p-8 rounded-lg border border-gray-300/20 dark:border-gray-700/30 shadow-lg">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-6xl font-bold text-primary-600 dark:text-primary-400 mb-4">500</h1>
|
||||||
|
<h2 class="text-2xl font-semibold mb-4">Interner Serverfehler</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-8">Es ist ein Fehler auf unserem Server aufgetreten. Unser Team wurde informiert und arbeitet bereits an einer Lösung. Bitte versuchen Sie es später erneut.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<a href="{{ url_for('index') }}" class="btn-primary">
|
||||||
|
<i class="fa-solid fa-home mr-2"></i>Zur Startseite
|
||||||
|
</a>
|
||||||
|
<a href="javascript:history.back()" class="btn-secondary">
|
||||||
|
<i class="fa-solid fa-arrow-left mr-2"></i>Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
64
templates/impressum.html
Normal file
64
templates/impressum.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Impressum{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
<div class="card p-6 md:p-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6 gradient-text">Impressum</h1>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Angaben gemäß § 5 TMG</h2>
|
||||||
|
<p class="mb-4">MindMap GmbH<br>
|
||||||
|
Musterstraße 123<br>
|
||||||
|
12345 Musterstadt<br>
|
||||||
|
Deutschland</p>
|
||||||
|
|
||||||
|
<p class="mb-4">
|
||||||
|
<strong>Vertreten durch:</strong><br>
|
||||||
|
Max Mustermann, Geschäftsführer
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-4">
|
||||||
|
<strong>Kontakt:</strong><br>
|
||||||
|
Telefon: +49 (0) 123 456789<br>
|
||||||
|
E-Mail: info@mindmap-example.com
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-4">
|
||||||
|
<strong>Registereintrag:</strong><br>
|
||||||
|
Eintragung im Handelsregister.<br>
|
||||||
|
Registergericht: Amtsgericht Musterstadt<br>
|
||||||
|
Registernummer: HRB 12345
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-4">
|
||||||
|
<strong>Umsatzsteuer-ID:</strong><br>
|
||||||
|
Umsatzsteuer-Identifikationsnummer gemäß §27 a Umsatzsteuergesetz:<br>
|
||||||
|
DE 123456789
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Redaktionell verantwortlich</h2>
|
||||||
|
<p>
|
||||||
|
Max Mustermann<br>
|
||||||
|
Musterstraße 123<br>
|
||||||
|
12345 Musterstadt
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Haftungsausschluss</h2>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-2">Haftung für Inhalte</h3>
|
||||||
|
<p class="mb-4">Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
|
||||||
|
<p class="mb-4">Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.</p>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-bold mb-2">Haftung für Links</h3>
|
||||||
|
<p class="mb-4">Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p>
|
||||||
|
<p>Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
603
templates/index.html
Normal file
603
templates/index.html
Normal file
@@ -0,0 +1,603 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Wissensnetzwerk{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Hintergrund über die gesamte Seite erstrecken */
|
||||||
|
html, body {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Entferne den Gradient-Hintergrund vollständig */
|
||||||
|
.hero-gradient, .bg-fade {
|
||||||
|
background: none !important;
|
||||||
|
clip-path: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-line {
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(to right, transparent, rgba(100, 100, 100, 0.1), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-dot {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(100, 100, 100, 0.2);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tech-line {
|
||||||
|
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tech-dot {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { r: 10; opacity: 0.7; }
|
||||||
|
50% { r: 12; opacity: 1; }
|
||||||
|
100% { r: 10; opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes iconPulse {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-pulse {
|
||||||
|
animation: iconPulse 3s ease-in-out infinite;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Volle Seitenbreite für Container */
|
||||||
|
#app-container, .container, main, .mx-auto, .py-12 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sicherstellen dass der Hintergrund die ganze Seite abdeckt */
|
||||||
|
.full-page-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat-Animationen */
|
||||||
|
.typing-dots span {
|
||||||
|
animation-duration: 1.2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat-Nachrichten-Animation */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(0, 10px, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#embedded-chat-messages > div {
|
||||||
|
animation: fadeInUp 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sanftes Scrollen im Chat */
|
||||||
|
#embedded-chat-messages {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Benutzerdefinierter Scrollbar für den Chat */
|
||||||
|
#embedded-chat-messages::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#embedded-chat-messages::-webkit-scrollbar-track {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#embedded-chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(139, 92, 246, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark #embedded-chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(139, 92, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover-Effekt für Quick-Query-Buttons */
|
||||||
|
.quick-query-btn:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: linear-gradient(to right, rgba(139, 92, 246, 0.1), rgba(96, 165, 250, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .quick-query-btn:hover {
|
||||||
|
background: linear-gradient(to right, rgba(139, 92, 246, 0.2), rgba(96, 165, 250, 0.2));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Hintergrund für die gesamte Seite -->
|
||||||
|
<div class="full-page-bg gradient-background"></div>
|
||||||
|
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="relative pt-20 pb-32">
|
||||||
|
<!-- Hero Content -->
|
||||||
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div class="text-center mb-16">
|
||||||
|
<h1 class="hero-heading mb-8 text-gray-900 dark:text-white">
|
||||||
|
<span class="gradient-text inline-block transform transition-all duration-700 hover:scale-105">Wissen</span> neu
|
||||||
|
<div class="mt-2 relative">
|
||||||
|
<span class="relative inline-block">vernetzen
|
||||||
|
<div class="absolute -bottom-2 left-0 right-0 h-1 bg-gradient-to-r from-purple-500/0 via-purple-500/70 to-purple-500/0 rounded-full"></div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl md:text-2xl text-gray-700 dark:text-gray-300 max-w-3xl mx-auto mb-12">
|
||||||
|
Erkunde komplexe Ideen visuell, schaffe Verbindungen und teile deine Gedanken
|
||||||
|
in einem interaktiven Wissensnetzwerk.
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-5 justify-center">
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="group transition-all duration-300 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-purple-500/20 transform hover:-translate-y-1">
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-diagram-project mr-3 text-purple-200 group-hover:text-white transition-all duration-300 animate-pulse"></i>
|
||||||
|
<span class="relative">
|
||||||
|
Mindmap erkunden
|
||||||
|
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% if not current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('register') }}" class="group transition-all duration-300 bg-gradient-to-r from-blue-500 to-cyan-500 hover:from-blue-600 hover:to-cyan-600 text-white font-medium text-lg px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl hover:shadow-blue-500/20 transform hover:-translate-y-1">
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-user-plus mr-3 text-blue-200 group-hover:text-white transition-all duration-300 icon-pulse"></i>
|
||||||
|
<span class="relative">
|
||||||
|
Konto erstellen
|
||||||
|
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-white group-hover:w-full transition-all duration-300"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tech illustration -->
|
||||||
|
<div class="relative w-full max-w-4xl mx-auto h-80 sm:h-96">
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div class="hidden md:block text-center">
|
||||||
|
<div class="text-3xl font-bold gradient-text mb-2 animate-float">Systades</div>
|
||||||
|
<div class="text-lg text-gray-700 dark:text-gray-300">WISSEN VERNETZEN</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Network Visualization with SVG -->
|
||||||
|
<svg class="absolute inset-0 w-full h-full" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<!-- Glossy Nodes and Lines -->
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="nodeGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
||||||
|
<stop offset="0%" stop-color="rgba(139, 92, 246, 0.9)" />
|
||||||
|
<stop offset="100%" stop-color="rgba(79, 70, 229, 0.5)" />
|
||||||
|
</radialGradient>
|
||||||
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="5" result="blur" />
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Network Lines -->
|
||||||
|
<g class="lines">
|
||||||
|
<!-- Connection network -->
|
||||||
|
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
||||||
|
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
||||||
|
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
||||||
|
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
||||||
|
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
||||||
|
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(0,0,0,0.1)" stroke-width="1" class="dark:hidden" />
|
||||||
|
|
||||||
|
<!-- Dark mode connections -->
|
||||||
|
<line x1="200" y1="250" x2="400" y2="150" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
||||||
|
<line x1="400" y1="150" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
||||||
|
<line x1="600" y1="250" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
||||||
|
<line x1="400" y1="350" x2="200" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
||||||
|
<line x1="400" y1="150" x2="400" y2="350" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
||||||
|
<line x1="200" y1="250" x2="600" y2="250" stroke="rgba(255,255,255,0.1)" stroke-width="1" class="hidden dark:inline" />
|
||||||
|
|
||||||
|
<!-- Pulse animation for some lines -->
|
||||||
|
<line class="animate-pulse" x1="400" y1="150" x2="300" y2="200" stroke="rgba(139, 92, 246, 0.5)" stroke-width="2" />
|
||||||
|
<line class="animate-pulse" x1="400" y1="350" x2="500" y2="300" stroke="rgba(168, 85, 247, 0.5)" stroke-width="2" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Network Nodes -->
|
||||||
|
<g class="nodes">
|
||||||
|
<circle cx="400" cy="150" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
|
||||||
|
<circle cx="200" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
|
||||||
|
<circle cx="600" cy="250" r="10" fill="url(#nodeGradient)" class="float-animation" />
|
||||||
|
<circle cx="400" cy="350" r="15" fill="url(#nodeGradient)" filter="url(#glow)" class="animate-pulse float-animation" />
|
||||||
|
<circle cx="300" cy="200" r="8" fill="url(#nodeGradient)" class="float-animation" />
|
||||||
|
<circle cx="500" cy="300" r="8" fill="url(#nodeGradient)" class="float-animation" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Features Section -->
|
||||||
|
<section class="py-20 relative">
|
||||||
|
<div class="tech-line absolute top-0 left-1/2 transform -translate-x-1/2 w-1/3"></div>
|
||||||
|
|
||||||
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-16">
|
||||||
|
<h2 class="section-heading mb-4 text-gray-900 dark:text-white">Was ist <span class="gradient-text">Systades?</span></h2>
|
||||||
|
<p class="text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||||
|
Ein modernes Werkzeug zum Visualisieren, Erforschen und Teilen von Wissen
|
||||||
|
in einer intuitiven, interaktiven Umgebung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
<!-- Feature Card 1 -->
|
||||||
|
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
||||||
|
<div class="icon mb-6 rounded-2xl shadow-lg">
|
||||||
|
<i class="fa-solid fa-brain"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3">Visualisiere Wissen</h3>
|
||||||
|
<p>
|
||||||
|
Sieh Wissen als vernetztes System, entdecke Zusammenhänge und erkenne überraschende
|
||||||
|
Verbindungen zwischen verschiedenen Themengebieten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Card 2 -->
|
||||||
|
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
||||||
|
<div class="icon mb-6 rounded-2xl shadow-lg">
|
||||||
|
<i class="fa-solid fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3">Teile Gedanken</h3>
|
||||||
|
<p>
|
||||||
|
Füge deine eigenen Ideen und Perspektiven hinzu. Erstelle Verbindungen zu
|
||||||
|
vorhandenen Gedanken und bereichere die wachsende Wissensbasis.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Card 3 -->
|
||||||
|
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
||||||
|
<div class="icon mb-6 rounded-2xl shadow-lg">
|
||||||
|
<i class="fa-solid fa-users"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3">Community</h3>
|
||||||
|
<p>
|
||||||
|
Sei Teil einer Gemeinschaft, die gemeinsam ein verteiltes Wissensarchiv aufbaut
|
||||||
|
und sich in thematisch fokussierten Bereichen austauscht.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Card 4 -->
|
||||||
|
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
||||||
|
<div class="icon mb-6 rounded-2xl shadow-lg">
|
||||||
|
<i class="fa-solid fa-robot"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3">KI-Assistenz</h3>
|
||||||
|
<p>
|
||||||
|
Lass dir von künstlicher Intelligenz helfen, neue Zusammenhänge zu entdecken,
|
||||||
|
Inhalte zusammenzufassen und Fragen zu beantworten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Card 5 -->
|
||||||
|
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
||||||
|
<div class="icon mb-6 rounded-2xl shadow-lg">
|
||||||
|
<i class="fa-solid fa-search"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3">Intelligente Suche</h3>
|
||||||
|
<p>
|
||||||
|
Finde genau die Informationen, die du suchst, mit fortschrittlichen Such- und
|
||||||
|
Filterfunktionen für eine präzise Navigation durch das Wissen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Card 6 -->
|
||||||
|
<div class="feature-card p-8 rounded-3xl hover:-translate-y-3 transform transition-all duration-300">
|
||||||
|
<div class="icon mb-6 rounded-2xl shadow-lg">
|
||||||
|
<i class="fa-solid fa-route"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-3">Geführte Pfade</h3>
|
||||||
|
<p>
|
||||||
|
Folge kuratierten Lernpfaden durch komplexe Themen oder erschaffe selbst
|
||||||
|
Routen für andere, die deinen Gedankengängen folgen möchten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Call to Action Section -->
|
||||||
|
<section class="py-16 sm:py-20 md:py-24 relative overflow-hidden">
|
||||||
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||||
|
<div class="glass-effect p-6 sm:p-8 md:p-12 rounded-3xl transform transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl bg-gradient-to-br from-purple-500/15 to-blue-500/15 backdrop-blur-xl border border-white/10 shadow-lg">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
||||||
|
<div class="md:w-2/3">
|
||||||
|
<h2 class="text-2xl sm:text-3xl lg:text-4xl font-bold mb-3 text-gray-900 dark:text-white leading-tight">
|
||||||
|
Bereit, <span class="gradient-text bg-clip-text text-transparent bg-gradient-to-r from-purple-500 to-blue-500">Wissen</span> neu zu entdecken?
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-base sm:text-lg mb-6 md:mb-0 max-w-2xl">
|
||||||
|
Starte jetzt deine Reise durch das Wissensnetzwerk und erschließe neue Perspektiven.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="md:w-1/3 text-center md:text-right">
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="inline-flex items-center justify-center w-full md:w-auto btn-primary font-bold py-3 sm:py-3.5 px-6 sm:px-8 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105 bg-gradient-to-r from-purple-600 to-blue-600 text-white">
|
||||||
|
<span class="flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-arrow-right mr-2"></i>
|
||||||
|
<span>Zur Mindmap</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Quick Access Section -->
|
||||||
|
<section class="py-16 sm:py-20">
|
||||||
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
||||||
|
<!-- Themen-Übersicht -->
|
||||||
|
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-3 hover:shadow-xl border border-white/10 backdrop-blur-md">
|
||||||
|
<h3 class="text-xl font-bold mb-4 flex items-center text-gray-800 dark:text-white">
|
||||||
|
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-violet-500 to-fuchsia-500 flex items-center justify-center mr-3 sm:mr-4 shadow-md transform transition-transform duration-300 hover:scale-110">
|
||||||
|
<i class="fa-solid fa-fire text-white text-base sm:text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg sm:text-xl md:text-2xl">Themen-Übersicht</span>
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3 sm:space-y-4 mb-6">
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-purple-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-medium text-gray-800 dark:text-gray-200">Wissensbereiche <span class="text-xs text-gray-500">(12)</span></p>
|
||||||
|
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Überblick über Themenbereiche</p>
|
||||||
|
</div>
|
||||||
|
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('search_thoughts_page') }}" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-blue-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-medium text-gray-800 dark:text-gray-200">Gedanken <span class="text-xs text-gray-500">(87)</span></p>
|
||||||
|
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Konkrete Einträge durchsuchen</p>
|
||||||
|
</div>
|
||||||
|
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
||||||
|
</a>
|
||||||
|
<a href="#" class="flex items-center p-3 sm:p-3.5 rounded-xl hover:bg-gray-100/50 dark:hover:bg-white/5 transition-all duration-200 group">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-green-400 mr-3 group-hover:scale-125 transition-transform"></div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<p class="font-medium text-gray-800 dark:text-gray-200">Verbindungen <span class="text-xs text-gray-500">(34)</span></p>
|
||||||
|
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">Beziehungen zwischen Gedanken</p>
|
||||||
|
</div>
|
||||||
|
<i class="fa-solid fa-chevron-right text-gray-500 group-hover:translate-x-1 transition-transform"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('search_thoughts_page') }}" class="btn-primary w-full text-center rounded-xl py-3 sm:py-3.5 transform transition-all duration-300 hover:-translate-y-1 hover:shadow-lg flex items-center justify-center">
|
||||||
|
<span>Alle Themen entdecken</span>
|
||||||
|
<i class="fa-solid fa-arrow-right ml-2"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KI-Assistent mit eingebettetem Chat -->
|
||||||
|
<div class="glass-morphism p-6 sm:p-8 rounded-3xl transition-all duration-500 hover:-translate-y-1 hover:shadow-xl backdrop-blur-md border border-white/10">
|
||||||
|
<h3 class="text-xl md:text-2xl font-bold mb-4 flex flex-wrap sm:flex-nowrap items-center text-gray-800 dark:text-white">
|
||||||
|
<div class="w-10 h-10 sm:w-12 sm:h-12 rounded-2xl bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center mr-3 sm:mr-4 shadow-lg transform transition-transform duration-300 hover:scale-110">
|
||||||
|
<i class="fa-solid fa-robot text-white text-base sm:text-lg"></i>
|
||||||
|
</div>
|
||||||
|
<span class="mt-1 sm:mt-0">KI-Assistent</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Eingebettetes Chat-Interface -->
|
||||||
|
<div id="embedded-assistant" class="rounded-xl border border-gray-200/50 dark:border-gray-700/50 overflow-hidden flex flex-col h-[300px]">
|
||||||
|
<!-- Chat Verlauf -->
|
||||||
|
<div id="embedded-chat-messages" class="flex-grow p-4 overflow-y-auto space-y-3 bg-white/70 dark:bg-gray-800/70">
|
||||||
|
<!-- Begrüßungsnachricht -->
|
||||||
|
<div class="flex items-start space-x-2">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fa-solid fa-robot text-white text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-200">Hallo! Ich bin dein KI-Assistent. Wie kann ich dir helfen?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Eingabe -->
|
||||||
|
<div class="p-3 border-t border-gray-200/70 dark:border-gray-700/70 bg-gray-50/90 dark:bg-gray-800/90">
|
||||||
|
<form id="embedded-chat-form" class="flex items-center space-x-2">
|
||||||
|
<input type="text" id="embedded-chat-input"
|
||||||
|
placeholder="Stelle eine Frage..."
|
||||||
|
class="flex-grow px-4 py-2 rounded-xl border bg-white/90 dark:bg-gray-700/90 border-gray-300 dark:border-gray-600 shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all duration-200 placeholder-gray-400 dark:placeholder-gray-500 text-gray-700 dark:text-gray-200">
|
||||||
|
<button type="submit"
|
||||||
|
class="p-2 rounded-xl bg-gradient-to-r from-purple-600 to-blue-600 text-white shadow-md hover:shadow-lg transition-all duration-200 hover:-translate-y-0.5">
|
||||||
|
<i class="fa-solid fa-paper-plane"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schnelllinks unter dem Chat -->
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
||||||
|
Was ist Systades?
|
||||||
|
</button>
|
||||||
|
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
||||||
|
Wie erstelle ich eine Mindmap?
|
||||||
|
</button>
|
||||||
|
<button class="quick-query-btn px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800/70 dark:hover:bg-gray-700/80 rounded-lg sm:rounded-xl text-xs text-gray-700 dark:text-gray-300 transition-all duration-200 hover:-translate-y-0.5 shadow-sm hover:shadow">
|
||||||
|
Zeige neueste Gedanken
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vollständigen KI-Chat öffnen -->
|
||||||
|
<button onclick="window.MindMap.assistant.toggleAssistant(true)" class="mt-4 btn-primary w-full text-center rounded-xl py-2 sm:py-2.5 shadow-md hover:shadow-lg transition-all duration-300 hover:-translate-y-1 flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-expand mr-2"></i>
|
||||||
|
<span>Chat in Vollansicht öffnen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- JavaScript für eingebetteten Chat -->
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Warten bis MindMap und der Assistent initialisiert sind
|
||||||
|
const waitForAssistant = setInterval(() => {
|
||||||
|
if (window.MindMap && window.MindMap.assistant) {
|
||||||
|
clearInterval(waitForAssistant);
|
||||||
|
initEmbeddedChat();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
function initEmbeddedChat() {
|
||||||
|
const chatForm = document.getElementById('embedded-chat-form');
|
||||||
|
const chatInput = document.getElementById('embedded-chat-input');
|
||||||
|
const messagesContainer = document.getElementById('embedded-chat-messages');
|
||||||
|
const quickQueryBtns = document.querySelectorAll('.quick-query-btn');
|
||||||
|
|
||||||
|
// Event-Listener für das Chat-Formular
|
||||||
|
chatForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const userMessage = chatInput.value.trim();
|
||||||
|
if (!userMessage) return;
|
||||||
|
|
||||||
|
// Nachricht des Benutzers anzeigen
|
||||||
|
appendMessage('user', userMessage);
|
||||||
|
chatInput.value = '';
|
||||||
|
|
||||||
|
// Anzeigen, dass der Assistent antwortet
|
||||||
|
const typingIndicator = appendTypingIndicator();
|
||||||
|
|
||||||
|
// API-Anfrage an den Assistenten senden
|
||||||
|
sendToAssistant(userMessage)
|
||||||
|
.then(response => {
|
||||||
|
// Entferne Tipp-Indikator
|
||||||
|
typingIndicator.remove();
|
||||||
|
// Zeige Antwort des Assistenten an
|
||||||
|
appendMessage('assistant', response);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
typingIndicator.remove();
|
||||||
|
appendMessage('assistant', 'Es tut mir leid, ich konnte deine Nachricht nicht verarbeiten. Bitte versuche es später noch einmal.');
|
||||||
|
console.error('Fehler bei der Kommunikation mit dem Assistenten:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schnellabfragen-Buttons
|
||||||
|
quickQueryBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const query = this.textContent.trim();
|
||||||
|
chatInput.value = query;
|
||||||
|
chatForm.dispatchEvent(new Event('submit'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum Hinzufügen einer Nachricht zum Chat
|
||||||
|
function appendMessage(sender, message) {
|
||||||
|
const messageElement = document.createElement('div');
|
||||||
|
messageElement.className = 'flex items-start space-x-2';
|
||||||
|
|
||||||
|
if (sender === 'user') {
|
||||||
|
messageElement.innerHTML = `
|
||||||
|
<div class="flex-grow"></div>
|
||||||
|
<div class="max-w-[85%] bg-blue-100 dark:bg-blue-900/40 p-3 rounded-xl rounded-tr-none shadow-sm">
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-indigo-500 flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fa-solid fa-user text-white text-xs"></i>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
messageElement.innerHTML = `
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fa-solid fa-robot text-white text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-200">${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesContainer.appendChild(messageElement);
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tipp-Indikator für "Assistent schreibt..."
|
||||||
|
function appendTypingIndicator() {
|
||||||
|
const indicatorElement = document.createElement('div');
|
||||||
|
indicatorElement.className = 'flex items-start space-x-2 typing-indicator';
|
||||||
|
indicatorElement.innerHTML = `
|
||||||
|
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 flex items-center justify-center flex-shrink-0">
|
||||||
|
<i class="fa-solid fa-robot text-white text-xs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-[85%] bg-purple-100 dark:bg-gray-700 p-3 rounded-xl rounded-tl-none shadow-sm">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
|
||||||
|
<span class="mr-1">Tipp</span>
|
||||||
|
<span class="typing-dots flex space-x-1">
|
||||||
|
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
|
||||||
|
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
|
||||||
|
<span class="w-1.5 h-1.5 bg-gray-500 dark:bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
messagesContainer.appendChild(indicatorElement);
|
||||||
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||||
|
return indicatorElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sende Nachricht an den Assistenten und erhalte Antwort
|
||||||
|
async function sendToAssistant(message) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/assistant', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: "Du bist ein hilfreicher Assistent für das Wissensnetzwerk Systades." },
|
||||||
|
{ role: "user", content: message }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Unbekannter Fehler');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.response || data.answer || 'Ich habe keine Antwort erhalten.';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler bei der API-Anfrage:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
11
templates/layout.html
Normal file
11
templates/layout.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<!-- Navigation -->
|
||||||
|
<header class="w-full">
|
||||||
|
<nav class="fixed top-0 z-50 w-full bg-dark-900 border-b border-gray-700">
|
||||||
|
<!-- ... existing code ... -->
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content Container -->
|
||||||
|
<div class="container mx-auto px-4 pt-20 pb-10">
|
||||||
|
<!-- ... existing code ... -->
|
||||||
|
</div>
|
||||||
55
templates/login.html
Normal file
55
templates/login.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Anmelden{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex justify-center items-center min-h-screen px-4 py-12">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<div class="bg-white bg-opacity-20 backdrop-blur-lg rounded-xl shadow-xl overflow-hidden transition-all duration-300 hover:shadow-2xl">
|
||||||
|
<div class="p-6 sm:p-8">
|
||||||
|
<h2 class="text-center text-2xl font-bold text-gray-800 mb-6">
|
||||||
|
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||||
|
Anmelden
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('login') }}" class="space-y-6">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-700">Benutzername</label>
|
||||||
|
<div class="relative rounded-md shadow-sm">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<i class="fas fa-user text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="username" name="username" required
|
||||||
|
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="Benutzername eingeben">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-700">Passwort</label>
|
||||||
|
<div class="relative rounded-md shadow-sm">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<i class="fas fa-lock text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<input type="password" id="password" name="password" required
|
||||||
|
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||||
|
placeholder="Passwort eingeben">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
|
||||||
|
<i class="fas fa-sign-in-alt mr-2"></i> Anmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center text-sm text-gray-600">
|
||||||
|
<p>Noch kein Konto? <a href="{{ url_for('register') }}" class="font-medium text-blue-600 hover:text-blue-500 transition-colors">Registrieren</a></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
1051
templates/mindmap.html
Normal file
1051
templates/mindmap.html
Normal file
File diff suppressed because it is too large
Load Diff
320
templates/my_account.html
Normal file
320
templates/my_account.html
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Meine Merkliste{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Hintergrund über die gesamte Seite erstrecken */
|
||||||
|
html, body {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-mindmap-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 1rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .personal-mindmap-container {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-item:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .empty-state {
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Hauptbereich -->
|
||||||
|
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 pt-8 pb-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
<span class="gradient-text">Meine Merkliste</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-700 dark:text-gray-300">
|
||||||
|
Deine persönliche Sammlung gemerkter Wissensbereiche und Beiträge
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Persönliche Mindmap -->
|
||||||
|
<div class="mb-12">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-4 flex items-center">
|
||||||
|
<i class="fas fa-project-diagram mr-3 text-purple-500"></i>
|
||||||
|
Meine Mindmap
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div id="personal-mindmap" class="personal-mindmap-container glass-morphism">
|
||||||
|
<div id="empty-mindmap-message" class="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div class="text-center p-6">
|
||||||
|
<div class="text-6xl mb-4 opacity-20">
|
||||||
|
<i class="fas fa-bookmark"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2 text-gray-700 dark:text-gray-300">Deine persönliche Mindmap ist leer</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
||||||
|
Merke dir Wissensbereiche und Gedanken in der Hauptmindmap, um deine persönliche Mindmap zu erstellen.
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="mt-4 inline-block px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors">
|
||||||
|
Zur Mindmap
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gemerkte Inhalte -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||||
|
<!-- Wissensbereiche -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-4 flex items-center">
|
||||||
|
<i class="fas fa-folder-open mr-3 text-blue-500"></i>
|
||||||
|
Gemerkte Wissensbereiche
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div id="bookmarked-areas-container" class="space-y-4">
|
||||||
|
<!-- Ladezustand -->
|
||||||
|
<div class="animate-pulse space-y-4">
|
||||||
|
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||||
|
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||||
|
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gedanken -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-4 flex items-center">
|
||||||
|
<i class="fas fa-lightbulb mr-3 text-amber-500"></i>
|
||||||
|
Gemerkte Gedanken
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div id="bookmarked-thoughts-container" class="space-y-4">
|
||||||
|
<!-- Ladezustand -->
|
||||||
|
<div class="animate-pulse space-y-4">
|
||||||
|
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||||
|
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||||
|
<div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript für persönliche Mindmap -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Lade gespeicherte Bookmarks aus dem LocalStorage
|
||||||
|
function loadBookmarkedNodes() {
|
||||||
|
try {
|
||||||
|
const bookmarked = localStorage.getItem('bookmarkedNodes');
|
||||||
|
return bookmarked ? JSON.parse(bookmarked) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der gemerkten Knoten:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarkedNodeIds = loadBookmarkedNodes();
|
||||||
|
|
||||||
|
// Prüfe, ob es gemerkte Knoten gibt
|
||||||
|
if (bookmarkedNodeIds && bookmarkedNodeIds.length > 0) {
|
||||||
|
// Verstecke die Leer-Nachricht
|
||||||
|
document.getElementById('empty-mindmap-message').style.display = 'none';
|
||||||
|
|
||||||
|
// Initialisiere die persönliche Mindmap
|
||||||
|
const personalMindmap = new MindMapVisualization('#personal-mindmap', {
|
||||||
|
width: document.getElementById('personal-mindmap').clientWidth,
|
||||||
|
height: 400,
|
||||||
|
nodeRadius: 18,
|
||||||
|
selectedNodeRadius: 22,
|
||||||
|
linkDistance: 120,
|
||||||
|
chargeStrength: -800,
|
||||||
|
centerForce: 0.1,
|
||||||
|
tooltipEnabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lade Daten für die Mindmap - hier müssten wir normalerweise die API nutzen,
|
||||||
|
// aber als Fallback laden wir die Standarddaten und filtern sie
|
||||||
|
window.setTimeout(() => {
|
||||||
|
// Wenn die Hauptmindmap geladen ist, können wir deren Daten verwenden
|
||||||
|
if (window.mindmapInstance) {
|
||||||
|
// Filtere nur gemerkte Knoten und ihre Verbindungen
|
||||||
|
const nodes = window.mindmapInstance.nodes.filter(node =>
|
||||||
|
bookmarkedNodeIds.includes(node.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Finde Verbindungen zwischen den gemerkten Knoten
|
||||||
|
const links = window.mindmapInstance.links.filter(link =>
|
||||||
|
bookmarkedNodeIds.includes(link.source.id || link.source) &&
|
||||||
|
bookmarkedNodeIds.includes(link.target.id || link.target)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setze Daten und aktualisiere die Visualisierung
|
||||||
|
personalMindmap.nodes = nodes;
|
||||||
|
personalMindmap.links = links;
|
||||||
|
personalMindmap.isLoading = false;
|
||||||
|
personalMindmap.updateVisualization();
|
||||||
|
} else {
|
||||||
|
// Fallback: Leere Mindmap anzeigen
|
||||||
|
document.getElementById('empty-mindmap-message').style.display = 'flex';
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
|
||||||
|
// Lade die gemerkten Inhalte
|
||||||
|
loadBookmarkedContent(bookmarkedNodeIds);
|
||||||
|
} else {
|
||||||
|
// Zeige Leerzustand an
|
||||||
|
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||||
|
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||||
|
|
||||||
|
areasContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Funktion zum Laden der gemerkten Inhalte
|
||||||
|
function loadBookmarkedContent(nodeIds) {
|
||||||
|
if (!nodeIds || nodeIds.length === 0) return;
|
||||||
|
|
||||||
|
// In einer vollständigen Implementierung würden wir hier einen API-Aufruf machen
|
||||||
|
// Für diese Demo erstellen wir Beispielinhalte
|
||||||
|
|
||||||
|
const areasContainer = document.getElementById('bookmarked-areas-container');
|
||||||
|
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
|
||||||
|
|
||||||
|
// Verschiedene Beispiel-Farben
|
||||||
|
const colors = ['purple', 'blue', 'green', 'indigo', 'amber'];
|
||||||
|
|
||||||
|
// Leere die Container
|
||||||
|
areasContainer.innerHTML = '';
|
||||||
|
thoughtsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Beispielinhalte für Wissensbereiche
|
||||||
|
const areaTemplates = [
|
||||||
|
{ name: 'Philosophie', description: 'Grundlagen philosophischen Denkens', count: 24 },
|
||||||
|
{ name: 'Wissenschaft', description: 'Wissenschaftliche Methoden und Erkenntnisse', count: 42 },
|
||||||
|
{ name: 'Technologie', description: 'Zukunftsweisende Technologien', count: 36 },
|
||||||
|
{ name: 'Kunst', description: 'Künstlerische Ausdrucksformen', count: 18 },
|
||||||
|
{ name: 'Psychologie', description: 'Menschliches Verhalten verstehen', count: 30 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Beispielinhalte für Gedanken
|
||||||
|
const thoughtTemplates = [
|
||||||
|
{ title: 'Quantenphysik und Bewusstsein', author: 'Maria Schmidt', date: '12.04.2023' },
|
||||||
|
{ title: 'Ethik in der künstlichen Intelligenz', author: 'Thomas Weber', date: '23.02.2023' },
|
||||||
|
{ title: 'Die Rolle der Kunst in der Gesellschaft', author: 'Lena Müller', date: '05.06.2023' },
|
||||||
|
{ title: 'Nachhaltige Entwicklung im 21. Jahrhundert', author: 'Michael Bauer', date: '18.08.2023' },
|
||||||
|
{ title: 'Kognitive Verzerrungen im Alltag', author: 'Sophie Klein', date: '30.09.2023' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Zeige nur so viele Elemente wie wir Knoten haben (bis zu 5)
|
||||||
|
const areaCount = Math.min(nodeIds.length, 5);
|
||||||
|
|
||||||
|
if (areaCount > 0) {
|
||||||
|
// Wissensbereiche hinzufügen
|
||||||
|
for (let i = 0; i < areaCount; i++) {
|
||||||
|
const area = areaTemplates[i];
|
||||||
|
const colorClass = colors[i % colors.length];
|
||||||
|
|
||||||
|
areasContainer.innerHTML += `
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||||
|
<i class="fas fa-bookmark text-${colorClass}-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">${area.name}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">${area.description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
${area.count} Einträge
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
areasContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Wissensbereiche</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeige bis zu 5 Gedanken an (oder weniger, falls weniger gemerkt wurden)
|
||||||
|
const thoughtCount = Math.min(nodeIds.length, 5);
|
||||||
|
|
||||||
|
if (thoughtCount > 0) {
|
||||||
|
// Gedanken hinzufügen
|
||||||
|
for (let i = 0; i < thoughtCount; i++) {
|
||||||
|
const thought = thoughtTemplates[i];
|
||||||
|
const colorClass = colors[(i + 2) % colors.length]; // Andere Farben als für Bereiche
|
||||||
|
|
||||||
|
thoughtsContainer.innerHTML += `
|
||||||
|
<a href="{{ url_for('mindmap') }}" class="bookmark-item block p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-${colorClass}-100 dark:bg-${colorClass}-900/30 flex items-center justify-center mr-3">
|
||||||
|
<i class="fas fa-lightbulb text-${colorClass}-500"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-white">${thought.title}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Von ${thought.author} • ${thought.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thoughtsContainer.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="text-4xl mb-2 opacity-20">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Keine gemerkten Gedanken</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
953
templates/profile.html
Normal file
953
templates/profile.html
Normal file
@@ -0,0 +1,953 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Profil{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Grundstile für das Profil mit verbessertem Glasmorphismus */
|
||||||
|
.profile-container {
|
||||||
|
background: rgba(24, 28, 45, 0.75);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35);
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-container:hover {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Profile-Header mit dynamischer User-Anzeige */
|
||||||
|
.profile-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
gap: 2.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(32, 36, 55, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border: 3px solid rgba(179, 143, 255, 0.3);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25), 0 0 15px rgba(179, 143, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
border: 3px solid rgba(179, 143, 255, 0.5);
|
||||||
|
box-shadow: 0 12px 45px rgba(0, 0, 0, 0.3), 0 0 25px rgba(179, 143, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar:hover img {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar-placeholder {
|
||||||
|
font-size: 5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar:hover .profile-avatar-placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-name {
|
||||||
|
font-size: 2.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #b38fff, #58a9ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
text-shadow: 0 2px 10px rgba(179, 143, 255, 0.3);
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-username {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-badge {
|
||||||
|
background: rgba(179, 143, 255, 0.2);
|
||||||
|
border: 1px solid rgba(179, 143, 255, 0.3);
|
||||||
|
color: #b38fff;
|
||||||
|
padding: 0.3rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username-badge:hover {
|
||||||
|
background: rgba(179, 143, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2), 0 0 8px rgba(179, 143, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-bio {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
max-width: 650px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-meta-item:hover {
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-meta-icon {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Statistik-Karten mit interaktiven Effekten */
|
||||||
|
.profile-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.75rem 1.25rem;
|
||||||
|
background: rgba(32, 36, 55, 0.7);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(circle at center, rgba(179, 143, 255, 0.15) 0%, transparent 70%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
background: rgba(32, 36, 55, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.25), 0 0 20px rgba(179, 143, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #b38fff, #58a9ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover .stat-value {
|
||||||
|
transform: scale(1.1);
|
||||||
|
text-shadow: 0 0 15px rgba(179, 143, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover .stat-label {
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat-Icon für visuelle Verstärkung */
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: rgba(179, 143, 255, 0.7);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover .stat-icon {
|
||||||
|
transform: scale(1.2) translateY(-3px);
|
||||||
|
color: rgba(179, 143, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Benutzer-Aktionsbereich */
|
||||||
|
.profile-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-action-btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(32, 36, 55, 0.75);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-action-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
background: rgba(179, 143, 255, 0.25);
|
||||||
|
border: 1px solid rgba(179, 143, 255, 0.3);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2), 0 0 15px rgba(179, 143, 255, 0.2);
|
||||||
|
text-shadow: 0 2px 3px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-action-btn.primary {
|
||||||
|
background: rgba(179, 143, 255, 0.25);
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(179, 143, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-action-btn.primary:hover {
|
||||||
|
background: rgba(179, 143, 255, 0.35);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25), 0 0 20px rgba(179, 143, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Profil-Tabs */
|
||||||
|
.profile-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(179, 143, 255, 0.5) rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-tab.active {
|
||||||
|
color: #b38fff;
|
||||||
|
border-bottom: 3px solid #b38fff;
|
||||||
|
background: rgba(179, 143, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aktivitäten und Beiträge */
|
||||||
|
.activity-feed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-card {
|
||||||
|
background: rgba(32, 36, 55, 0.7);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25), 0 0 15px rgba(179, 143, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-header {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-date {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-reactions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-button:hover {
|
||||||
|
background: rgba(179, 143, 255, 0.15);
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-button.active {
|
||||||
|
background: rgba(179, 143, 255, 0.2);
|
||||||
|
color: #b38fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: rgba(179, 143, 255, 0.1);
|
||||||
|
color: #b38fff;
|
||||||
|
border: 1px solid rgba(179, 143, 255, 0.25);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background: rgba(179, 143, 255, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 0 10px rgba(179, 143, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Einstellungskarten */
|
||||||
|
.settings-card {
|
||||||
|
background: rgba(32, 36, 55, 0.7);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card:hover {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2), 0 0 15px rgba(179, 143, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-header {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: rgba(24, 28, 45, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input:focus {
|
||||||
|
border-color: rgba(179, 143, 255, 0.4);
|
||||||
|
box-shadow: 0 0 0 3px rgba(179, 143, 255, 0.15);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Mode Anpassungen */
|
||||||
|
html.light .profile-container,
|
||||||
|
html.light .profile-tabs,
|
||||||
|
html.light .activity-card,
|
||||||
|
html.light .settings-card {
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .profile-avatar {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 3px solid rgba(126, 63, 242, 0.3);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), 0 0 15px rgba(126, 63, 242, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .profile-name {
|
||||||
|
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .profile-username,
|
||||||
|
html.light .profile-bio,
|
||||||
|
html.light .activity-content {
|
||||||
|
color: #1a202c;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .username-badge {
|
||||||
|
background: rgba(126, 63, 242, 0.15);
|
||||||
|
border: 1px solid rgba(126, 63, 242, 0.3);
|
||||||
|
color: #7e3ff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .profile-meta {
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .stat-item,
|
||||||
|
html.light .settings-input {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .stat-value {
|
||||||
|
background: linear-gradient(135deg, #7e3ff2, #3282f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .stat-label {
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .profile-tab {
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .profile-tab:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .profile-tab.active {
|
||||||
|
color: #7e3ff2;
|
||||||
|
border-bottom: 3px solid #7e3ff2;
|
||||||
|
background: rgba(126, 63, 242, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .activity-title,
|
||||||
|
html.light .settings-card-header,
|
||||||
|
html.light .settings-label {
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .activity-date {
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .activity-footer {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .reaction-button {
|
||||||
|
color: #4a5568;
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .reaction-button:hover {
|
||||||
|
background: rgba(126, 63, 242, 0.1);
|
||||||
|
color: #7e3ff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .reaction-button.active {
|
||||||
|
background: rgba(126, 63, 242, 0.15);
|
||||||
|
color: #7e3ff2;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .action-button {
|
||||||
|
background: rgba(126, 63, 242, 0.1);
|
||||||
|
color: #7e3ff2;
|
||||||
|
border: 1px solid rgba(126, 63, 242, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.light .action-button:hover {
|
||||||
|
background: rgba(126, 63, 242, 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mx-auto px-4 py-10">
|
||||||
|
<!-- Profil-Container -->
|
||||||
|
<div class="profile-container">
|
||||||
|
<!-- Profil-Header mit Benutzerinformationen -->
|
||||||
|
<div class="profile-header">
|
||||||
|
<!-- Profilbild -->
|
||||||
|
<div class="profile-avatar">
|
||||||
|
{% if user.profile_image %}
|
||||||
|
<img src="{{ user.profile_image }}" alt="{{ user.name }}" />
|
||||||
|
{% else %}
|
||||||
|
<div class="profile-avatar-placeholder">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profilinformationen -->
|
||||||
|
<div class="profile-info">
|
||||||
|
<h1 class="profile-name">{{ user.name|default('Max Mustermann') }}</h1>
|
||||||
|
<div class="profile-username">
|
||||||
|
@{{ user.username|default('maxmustermann') }}
|
||||||
|
{% if user.verified %}
|
||||||
|
<span class="username-badge">
|
||||||
|
<i class="fas fa-check-circle mr-1"></i> Verifiziert
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="profile-bio">
|
||||||
|
{{ user.bio|default('Willkommen auf meinem Profil! Ich bin daran interessiert, Wissen zu vernetzen und neue Verbindungen zwischen verschiedenen Themengebieten zu entdecken. Mein Ziel ist es, ein tieferes Verständnis für komplexe Zusammenhänge zu entwickeln.') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Meta-Informationen -->
|
||||||
|
<div class="profile-meta">
|
||||||
|
<div class="profile-meta-item">
|
||||||
|
<i class="fas fa-map-marker-alt profile-meta-icon"></i>
|
||||||
|
<span>{{ user.location|default('Berlin, Deutschland') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-meta-item">
|
||||||
|
<i class="fas fa-calendar-alt profile-meta-icon"></i>
|
||||||
|
<span>Mitglied seit {{ user.joined_date|default('Januar 2023') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="profile-meta-item">
|
||||||
|
<i class="fas fa-globe profile-meta-icon"></i>
|
||||||
|
<span>{{ user.website|default('www.beispiel.de') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profil-Aktionen -->
|
||||||
|
<div class="profile-actions">
|
||||||
|
<button class="profile-action-btn primary">
|
||||||
|
<i class="fas fa-user-plus mr-1"></i> Folgen
|
||||||
|
</button>
|
||||||
|
<button class="profile-action-btn">
|
||||||
|
<i class="fas fa-comment mr-1"></i> Nachricht
|
||||||
|
</button>
|
||||||
|
<button class="profile-action-btn">
|
||||||
|
<i class="fas fa-share-alt mr-1"></i> Teilen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiken -->
|
||||||
|
<div class="profile-stats">
|
||||||
|
<!-- Gedanken -->
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-lightbulb"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value">{{ user.thoughts_count|default('42') }}</div>
|
||||||
|
<div class="stat-label">Gedanken</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verbindungen -->
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-project-diagram"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value">{{ user.connections_count|default('128') }}</div>
|
||||||
|
<div class="stat-label">Verbindungen</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Follower -->
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value">{{ user.followers_count|default('567') }}</div>
|
||||||
|
<div class="stat-label">Follower</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beiträge -->
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-comment-dots"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value">{{ user.contributions_count|default('89') }}</div>
|
||||||
|
<div class="stat-label">Beiträge</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bewertung -->
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-value">{{ user.rating|default('4.8') }}</div>
|
||||||
|
<div class="stat-label">Bewertung</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs und Inhaltsbereich -->
|
||||||
|
<div class="glass-card">
|
||||||
|
<!-- Profil-Tabs -->
|
||||||
|
<div class="profile-tabs">
|
||||||
|
<div class="profile-tab active" data-tab="activity">Aktivitäten</div>
|
||||||
|
<div class="profile-tab" data-tab="thoughts">Gedanken</div>
|
||||||
|
<div class="profile-tab" data-tab="collections">Sammlungen</div>
|
||||||
|
<div class="profile-tab" data-tab="connections">Verbindungen</div>
|
||||||
|
<div class="profile-tab" data-tab="settings">Einstellungen</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab-Inhalte -->
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Aktivitäts-Tab (Standardmäßig angezeigt) -->
|
||||||
|
<div class="tab-content" id="activity-tab">
|
||||||
|
<div class="activity-feed">
|
||||||
|
<!-- Aktivität 1 -->
|
||||||
|
<div class="activity-card">
|
||||||
|
<div class="activity-header">
|
||||||
|
<div class="activity-title">Neuer Gedanke hinzugefügt</div>
|
||||||
|
<div class="activity-date">vor 2 Stunden</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-content">
|
||||||
|
<p>Ich habe einen neuen Gedanken zum Thema "Künstliche Intelligenz und Kreativität" hinzugefügt. Wie können KI-Tools uns dabei helfen, kreativer zu denken?</p>
|
||||||
|
</div>
|
||||||
|
<div class="activity-footer">
|
||||||
|
<div class="activity-reactions">
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-thumbs-up"></i> <span>24</span>
|
||||||
|
</button>
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-comment"></i> <span>8</span>
|
||||||
|
</button>
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-share"></i> <span>3</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="activity-actions">
|
||||||
|
<button class="action-button">
|
||||||
|
Ansehen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktivität 2 -->
|
||||||
|
<div class="activity-card">
|
||||||
|
<div class="activity-header">
|
||||||
|
<div class="activity-title">Verbindung erstellt</div>
|
||||||
|
<div class="activity-date">vor 1 Tag</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-content">
|
||||||
|
<p>Ich habe eine neue Verbindung zwischen "Nachhaltige Entwicklung" und "Digitale Transformation" hergestellt. Es gibt interessante Überschneidungen in diesen Bereichen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="activity-footer">
|
||||||
|
<div class="activity-reactions">
|
||||||
|
<button class="reaction-button active">
|
||||||
|
<i class="fas fa-thumbs-up"></i> <span>42</span>
|
||||||
|
</button>
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-comment"></i> <span>12</span>
|
||||||
|
</button>
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-share"></i> <span>7</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="activity-actions">
|
||||||
|
<button class="action-button">
|
||||||
|
Ansehen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktivität 3 -->
|
||||||
|
<div class="activity-card">
|
||||||
|
<div class="activity-header">
|
||||||
|
<div class="activity-title">Sammlung erstellt</div>
|
||||||
|
<div class="activity-date">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-content">
|
||||||
|
<p>Ich habe eine neue Sammlung zum Thema "Zukunftstechnologien" erstellt. Diese Sammlung enthält Gedanken zu KI, Quantencomputing, Biotechnologie und mehr.</p>
|
||||||
|
</div>
|
||||||
|
<div class="activity-footer">
|
||||||
|
<div class="activity-reactions">
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-thumbs-up"></i> <span>17</span>
|
||||||
|
</button>
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-comment"></i> <span>4</span>
|
||||||
|
</button>
|
||||||
|
<button class="reaction-button">
|
||||||
|
<i class="fas fa-share"></i> <span>2</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="activity-actions">
|
||||||
|
<button class="action-button">
|
||||||
|
Ansehen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Weitere Tab-Inhalte (werden per JavaScript angezeigt) -->
|
||||||
|
<div class="tab-content hidden" id="thoughts-tab">
|
||||||
|
<p class="text-center text-gray-400 py-12">
|
||||||
|
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
||||||
|
Gedanken werden geladen...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content hidden" id="collections-tab">
|
||||||
|
<p class="text-center text-gray-400 py-12">
|
||||||
|
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
||||||
|
Sammlungen werden geladen...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content hidden" id="connections-tab">
|
||||||
|
<p class="text-center text-gray-400 py-12">
|
||||||
|
<i class="fas fa-spinner fa-spin text-3xl mb-4 block"></i>
|
||||||
|
Verbindungen werden geladen...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content hidden" id="settings-tab">
|
||||||
|
<!-- Einstellungs-Tab-Inhalt -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<div class="settings-card-header">Profilinformationen</div>
|
||||||
|
<div class="settings-card-body">
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label" for="name">Name</label>
|
||||||
|
<input type="text" id="name" class="settings-input" value="{{ user.name|default('Max Mustermann') }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label" for="bio">Über mich</label>
|
||||||
|
<textarea id="bio" class="settings-input" rows="4">{{ user.bio|default('Willkommen auf meinem Profil! Ich bin daran interessiert, Wissen zu vernetzen und neue Verbindungen zwischen verschiedenen Themengebieten zu entdecken.') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label" for="location">Standort</label>
|
||||||
|
<input type="text" id="location" class="settings-input" value="{{ user.location|default('Berlin, Deutschland') }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label" for="website">Website</label>
|
||||||
|
<input type="url" id="website" class="settings-input" value="{{ user.website|default('https://www.beispiel.de') }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="profile-action-btn primary mt-4">
|
||||||
|
<i class="fas fa-save mr-1"></i> Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-card">
|
||||||
|
<div class="settings-card-header">Datenschutz und Sicherheit</div>
|
||||||
|
<div class="settings-card-body">
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label" for="email">E-Mail-Adresse</label>
|
||||||
|
<input type="email" id="email" class="settings-input" value="{{ user.email|default('beispiel@email.com') }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label" for="password">Neues Passwort</label>
|
||||||
|
<input type="password" id="password" class="settings-input" placeholder="Neues Passwort eingeben" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-group">
|
||||||
|
<label class="settings-label" for="password_confirm">Passwort bestätigen</label>
|
||||||
|
<input type="password" id="password_confirm" class="settings-input" placeholder="Passwort wiederholen" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="profile-action-btn primary mt-4">
|
||||||
|
<i class="fas fa-lock mr-1"></i> Passwort aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Profil-Tab-Funktionalität
|
||||||
|
const tabs = document.querySelectorAll('.profile-tab');
|
||||||
|
const tabContents = document.querySelectorAll('.tab-content');
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', function() {
|
||||||
|
// Alle Tabs deaktivieren
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
|
||||||
|
// Aktuellen Tab aktivieren
|
||||||
|
this.classList.add('active');
|
||||||
|
|
||||||
|
// Alle Tab-Inhalte ausblenden
|
||||||
|
tabContents.forEach(content => content.classList.add('hidden'));
|
||||||
|
|
||||||
|
// Entsprechenden Tab-Inhalt anzeigen
|
||||||
|
const tabId = this.getAttribute('data-tab');
|
||||||
|
document.getElementById(`${tabId}-tab`).classList.remove('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Like-Button-Funktionalität
|
||||||
|
const reactionButtons = document.querySelectorAll('.reaction-button');
|
||||||
|
|
||||||
|
reactionButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
// Toggle active-Klasse
|
||||||
|
this.classList.toggle('active');
|
||||||
|
|
||||||
|
// Aktueller Zählerstand
|
||||||
|
const countElement = this.querySelector('span');
|
||||||
|
let count = parseInt(countElement.textContent);
|
||||||
|
|
||||||
|
// Zähler aktualisieren
|
||||||
|
if (this.classList.contains('active')) {
|
||||||
|
count += 1;
|
||||||
|
} else {
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
countElement.textContent = count;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
115
templates/register.html
Normal file
115
templates/register.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Registrieren{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex justify-center items-center mt-10 px-4">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<div class="bg-white bg-opacity-80 backdrop-blur-lg rounded-lg shadow-md border border-white border-opacity-30 p-6 md:p-8 transition-all duration-300 transform hover:shadow-lg">
|
||||||
|
<h2 class="text-center mb-6 text-gray-800 font-bold text-2xl flex items-center justify-center">
|
||||||
|
<i class="fas fa-user-plus mr-2 text-blue-600"></i>
|
||||||
|
Registrieren
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('register') }}" class="needs-validation space-y-6" novalidate>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="username" class="block text-gray-700 font-medium text-sm">Benutzername</label>
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
<span class="absolute left-3 text-blue-600">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
|
||||||
|
id="username" name="username" placeholder="Dein Benutzername" required>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback text-red-600 text-sm hidden">
|
||||||
|
Bitte gib einen Benutzernamen ein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="email" class="block text-gray-700 font-medium text-sm">E-Mail</label>
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
<span class="absolute left-3 text-blue-600">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<input type="email" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
|
||||||
|
id="email" name="email" placeholder="name@beispiel.de" required>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback text-red-600 text-sm hidden">
|
||||||
|
Bitte gib eine gültige E-Mail-Adresse ein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="password" class="block text-gray-700 font-medium text-sm">Passwort</label>
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
<span class="absolute left-3 text-blue-600">
|
||||||
|
<i class="fas fa-lock"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password" class="pl-10 w-full rounded-md border border-gray-300 py-2 px-4 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200"
|
||||||
|
id="password" name="password" placeholder="Mindestens 8 Zeichen" required>
|
||||||
|
<button class="absolute right-2 text-gray-500 hover:text-gray-700 focus:outline-none" type="button" id="togglePassword">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback text-red-600 text-sm hidden">
|
||||||
|
Bitte gib ein sicheres Passwort ein.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-2">
|
||||||
|
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md shadow-sm transition-all duration-200 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
|
||||||
|
<i class="fas fa-user-plus mr-2"></i> Konto erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-4 text-gray-700 text-sm">
|
||||||
|
<p>Bereits registriert? <a href="{{ url_for('login') }}" class="text-blue-600 hover:text-blue-800 font-medium transition-colors duration-200">Anmelden</a></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Formularvalidierung aktivieren
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
var forms = document.querySelectorAll('.needs-validation');
|
||||||
|
Array.prototype.slice.call(forms).forEach(function(form) {
|
||||||
|
form.addEventListener('submit', function(event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Zeige Fehlermeldungen an
|
||||||
|
form.querySelectorAll(':invalid').forEach(function(input) {
|
||||||
|
input.parentNode.nextElementSibling.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
// Verstecke Fehlermeldungen bei Eingabe
|
||||||
|
form.querySelectorAll('input').forEach(function(input) {
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
if (this.checkValidity()) {
|
||||||
|
this.parentNode.nextElementSibling.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Passwort-Sichtbarkeit umschalten
|
||||||
|
const togglePassword = document.querySelector('#togglePassword');
|
||||||
|
const password = document.querySelector('#password');
|
||||||
|
|
||||||
|
togglePassword.addEventListener('click', function() {
|
||||||
|
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
|
||||||
|
password.setAttribute('type', type);
|
||||||
|
this.querySelector('i').classList.toggle('fa-eye');
|
||||||
|
this.querySelector('i').classList.toggle('fa-eye-slash');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
105
templates/search.html
Normal file
105
templates/search.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Suche{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="flex flex-col md:flex-row gap-6">
|
||||||
|
<!-- Filter-Sidebar -->
|
||||||
|
<div class="w-full md:w-1/3 lg:w-1/4 mb-6">
|
||||||
|
<div class="bg-white/10 backdrop-blur-md rounded-xl p-6 shadow-lg">
|
||||||
|
<h4 class="text-xl font-semibold mb-4">Erweiterte Suche</h4>
|
||||||
|
<form id="search-form">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-1">Suchbegriff</label>
|
||||||
|
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||||
|
<span class="px-3 py-2 text-gray-400">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="q" placeholder="Suche...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-1">Schlagworte</label>
|
||||||
|
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||||
|
<span class="px-3 py-2 text-gray-400">
|
||||||
|
<i class="fas fa-tags"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="keywords" placeholder="Schlagworte (kommagetrennt)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-1">Minimale Bewertung</label>
|
||||||
|
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||||
|
<span class="px-3 py-2 text-gray-400">
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
</span>
|
||||||
|
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="min_rating">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<option value="4">4+ Sterne</option>
|
||||||
|
<option value="3">3+ Sterne</option>
|
||||||
|
<option value="2">2+ Sterne</option>
|
||||||
|
<option value="1">1+ Stern</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium mb-1">Quellentyp</label>
|
||||||
|
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||||
|
<span class="px-3 py-2 text-gray-400">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
</span>
|
||||||
|
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="source_type">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<option value="PDF">PDF</option>
|
||||||
|
<option value="Markdown">Markdown</option>
|
||||||
|
<option value="Text">Text</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="block text-sm font-medium mb-1">Beziehungstyp</label>
|
||||||
|
<div class="flex items-center border rounded-lg overflow-hidden bg-white/5">
|
||||||
|
<span class="px-3 py-2 text-gray-400">
|
||||||
|
<i class="fas fa-project-diagram"></i>
|
||||||
|
</span>
|
||||||
|
<select class="w-full bg-transparent border-0 focus:ring-0 py-2 px-1" name="relation_type">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<option value="SUPPORTS">Stützt</option>
|
||||||
|
<option value="CONTRADICTS">Widerspricht</option>
|
||||||
|
<option value="BUILDS_UPON">Baut auf auf</option>
|
||||||
|
<option value="GENERALIZES">Verallgemeinert</option>
|
||||||
|
<option value="SPECIFIES">Spezifiziert</option>
|
||||||
|
<option value="INSPIRES">Inspiriert</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-indigo-600 text-white py-2 px-4 rounded-lg hover:opacity-90 transition-all flex items-center justify-center">
|
||||||
|
<i class="fas fa-search mr-2"></i> Suchen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Suchergebnisse -->
|
||||||
|
<div class="w-full md:w-2/3 lg:w-3/4">
|
||||||
|
<div class="bg-white/10 backdrop-blur-md rounded-xl p-6 shadow-lg mb-6">
|
||||||
|
<h3 class="text-2xl font-semibold mb-2">Suchergebnisse</h3>
|
||||||
|
<p class="text-gray-300 text-sm">Nutze die Filter links, um deine Suche zu präzisieren.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-results" class="mb-6">
|
||||||
|
<!-- Suchergebnisse werden hier dynamisch eingefügt -->
|
||||||
|
<div class="bg-white/10 backdrop-blur-md rounded-xl p-8 shadow-lg text-center">
|
||||||
|
<i class="fas fa-search text-5xl mb-6 text-blue-400"></i>
|
||||||
|
<h5 class="text-xl font-medium mb-3">Wissen entdecken</h5>
|
||||||
|
<p class="text-gray-300">Gib einen Suchbegriff ein, um in der wissenschaftlichen Wissensdatenbank zu suchen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
269
templates/settings.html
Normal file
269
templates/settings.html
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Einstellungen{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="relative mb-6" data-page="settings">
|
||||||
|
<!-- Header Bereich -->
|
||||||
|
<div class="mb-8 p-6 bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold mb-3">
|
||||||
|
<span class="gradient-text">Einstellungen</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-300 max-w-3xl">
|
||||||
|
Verwalten Sie Ihr Konto und passen Sie Ihre Benutzererfahrung an.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Einstellungen Grid Layout -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Seitennavigation (Desktop) -->
|
||||||
|
<div class="lg:col-span-1 order-2 lg:order-1">
|
||||||
|
<div class="sticky top-6 bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl overflow-hidden p-4">
|
||||||
|
<nav>
|
||||||
|
<div class="text-xs uppercase text-gray-400 font-semibold tracking-wider mb-3 px-3">Navigation</div>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li>
|
||||||
|
<button id="nav-account" class="w-full text-left px-3 py-2 rounded-md flex items-center text-purple-300 bg-purple-900/20 border-l-2 border-purple-500">
|
||||||
|
<i class="fa-solid fa-user-circle mr-3"></i>
|
||||||
|
<span>Konto</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="nav-appearance" class="w-full text-left px-3 py-2 rounded-md flex items-center text-gray-300 hover:bg-white/5">
|
||||||
|
<i class="fa-solid fa-palette mr-3"></i>
|
||||||
|
<span>Erscheinungsbild</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="nav-notifications" class="w-full text-left px-3 py-2 rounded-md flex items-center text-gray-300 hover:bg-white/5">
|
||||||
|
<i class="fa-solid fa-bell mr-3"></i>
|
||||||
|
<span>Benachrichtigungen</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button id="nav-privacy" class="w-full text-left px-3 py-2 rounded-md flex items-center text-gray-300 hover:bg-white/5">
|
||||||
|
<i class="fa-solid fa-shield-alt mr-3"></i>
|
||||||
|
<span>Datenschutz</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hauptinhalt der Einstellungen -->
|
||||||
|
<div class="lg:col-span-2 order-1 lg:order-2">
|
||||||
|
<!-- Konto-Einstellungen -->
|
||||||
|
<div id="section-account" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||||
|
<i class="fa-solid fa-user-circle text-purple-400 mr-3"></i>
|
||||||
|
Konto-Einstellungen
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="mb-6 p-4 bg-slate-900/50 rounded-lg">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="h-16 w-16 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center text-white text-2xl font-bold">
|
||||||
|
{{ current_user.username[0] | upper }}
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<h3 class="text-lg font-medium text-white">{{ current_user.username }}</h3>
|
||||||
|
<p class="text-gray-400">{{ current_user.email }}</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
Mitglied seit {{ current_user.id | string }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST" class="space-y-6">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-400 mb-1">Benutzername</label>
|
||||||
|
<input type="text" name="username" id="username" value="{{ current_user.username }}" disabled
|
||||||
|
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Der Benutzername kann derzeit nicht geändert werden.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-400 mb-1">E-Mail-Adresse</label>
|
||||||
|
<input type="email" name="email" id="email" value="{{ current_user.email }}" disabled
|
||||||
|
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Die E-Mail-Adresse kann derzeit nicht geändert werden.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-slate-700 pt-6">
|
||||||
|
<h3 class="text-lg font-medium text-white mb-4">Passwort ändern</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="current_password" class="block text-sm font-medium text-gray-400 mb-1">Aktuelles Passwort</label>
|
||||||
|
<input type="password" name="current_password" id="current_password"
|
||||||
|
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="new_password" class="block text-sm font-medium text-gray-400 mb-1">Neues Passwort</label>
|
||||||
|
<input type="password" name="new_password" id="new_password"
|
||||||
|
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||||
|
<p class="mt-1 text-xs text-gray-500">Mindestens 6 Zeichen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="confirm_password" class="block text-sm font-medium text-gray-400 mb-1">Passwort bestätigen</label>
|
||||||
|
<input type="password" name="confirm_password" id="confirm_password"
|
||||||
|
class="w-full bg-slate-800/80 border border-slate-700 rounded-md py-2 px-3 text-white focus:ring-1 focus:ring-purple-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="py-2 px-4 rounded-md bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white font-medium transition duration-300 transform hover:-translate-y-0.5">
|
||||||
|
Änderungen speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Erscheinungsbild -->
|
||||||
|
<div id="section-appearance" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6 hidden">
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||||
|
<i class="fa-solid fa-palette text-purple-400 mr-3"></i>
|
||||||
|
Erscheinungsbild
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="p-4 bg-slate-900/50 rounded-lg">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-white">Dunkles Design</h3>
|
||||||
|
<p class="text-gray-400">Wechsle zwischen hellem und dunklem Modus</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="toggle-dark-mode" class="w-14 h-7 flex items-center bg-purple-900/30 rounded-full px-1 transition-all" onclick="toggleDarkMode()">
|
||||||
|
<div id="dark-mode-indicator" class="bg-purple-500 w-5 h-5 rounded-full shadow-md transform transition-transform duration-300 translate-x-7"></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Benachrichtigungen -->
|
||||||
|
<div id="section-notifications" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6 hidden">
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||||
|
<i class="fa-solid fa-bell text-purple-400 mr-3"></i>
|
||||||
|
Benachrichtigungen
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="p-4 bg-slate-900/50 rounded-lg">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-medium text-white">E-Mail-Benachrichtigungen</h3>
|
||||||
|
<p class="text-gray-400">Diese Funktion wird in einer zukünftigen Version verfügbar sein.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="px-2 py-1 text-xs rounded-full bg-slate-700 text-gray-300">
|
||||||
|
Demnächst
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Datenschutz -->
|
||||||
|
<div id="section-privacy" class="bg-gradient-to-br from-slate-900/80 to-slate-800/60 rounded-lg border border-slate-700/20 shadow-xl p-6 mb-6 hidden">
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4 flex items-center">
|
||||||
|
<i class="fa-solid fa-shield-alt text-purple-400 mr-3"></i>
|
||||||
|
Datenschutz und Sicherheit
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="p-4 bg-slate-900/50 rounded-lg">
|
||||||
|
<h3 class="text-lg font-medium text-white mb-2">Datenverarbeitung</h3>
|
||||||
|
<p class="text-gray-400 mb-3">
|
||||||
|
Wir verarbeiten Ihre Daten gemäß unserer Datenschutzrichtlinie.
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('datenschutz') }}" class="text-purple-400 hover:text-purple-300 inline-flex items-center">
|
||||||
|
<span>Datenschutzerklärung lesen</span>
|
||||||
|
<i class="fa-solid fa-arrow-right ml-1 text-sm"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Navigation-Buttons
|
||||||
|
const navButtons = {
|
||||||
|
'nav-account': 'section-account',
|
||||||
|
'nav-appearance': 'section-appearance',
|
||||||
|
'nav-notifications': 'section-notifications',
|
||||||
|
'nav-privacy': 'section-privacy'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alle Abschnitte ausblenden außer dem ersten
|
||||||
|
Object.values(navButtons).forEach(sectionId => {
|
||||||
|
if (sectionId !== 'section-account') {
|
||||||
|
document.getElementById(sectionId).classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Klick-Handler für Navigation
|
||||||
|
for (const [navId, sectionId] of Object.entries(navButtons)) {
|
||||||
|
document.getElementById(navId).addEventListener('click', function() {
|
||||||
|
// Alle Abschnitte ausblenden
|
||||||
|
Object.values(navButtons).forEach(id => {
|
||||||
|
document.getElementById(id).classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ausgewählten Abschnitt anzeigen
|
||||||
|
document.getElementById(sectionId).classList.remove('hidden');
|
||||||
|
|
||||||
|
// Aktiven Navigations-Button hervorheben, andere zurücksetzen
|
||||||
|
for (const btnId of Object.keys(navButtons)) {
|
||||||
|
const btn = document.getElementById(btnId);
|
||||||
|
if (btnId === navId) {
|
||||||
|
btn.classList.add('text-purple-300', 'bg-purple-900/20', 'border-l-2', 'border-purple-500');
|
||||||
|
btn.classList.remove('text-gray-300', 'hover:bg-white/5');
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('text-purple-300', 'bg-purple-900/20', 'border-l-2', 'border-purple-500');
|
||||||
|
btn.classList.add('text-gray-300', 'hover:bg-white/5');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark Mode Toggle aktualisieren
|
||||||
|
function updateDarkModeToggle() {
|
||||||
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
const indicator = document.getElementById('dark-mode-indicator');
|
||||||
|
|
||||||
|
if (indicator) {
|
||||||
|
if (isDarkMode) {
|
||||||
|
indicator.classList.add('translate-x-7');
|
||||||
|
indicator.classList.remove('translate-x-0');
|
||||||
|
} else {
|
||||||
|
indicator.classList.add('translate-x-0');
|
||||||
|
indicator.classList.remove('translate-x-7');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDarkModeToggle();
|
||||||
|
|
||||||
|
// Globale Funktion für Toggle-Button
|
||||||
|
window.toggleDarkMode = function() {
|
||||||
|
if (typeof MindMap !== 'undefined' && typeof MindMap.toggleDarkMode === 'function') {
|
||||||
|
MindMap.toggleDarkMode();
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
}
|
||||||
|
updateDarkModeToggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
34
utils/__init__.py
Executable file
34
utils/__init__.py
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility functions for the website application.
|
||||||
|
This package contains various utilities for database management,
|
||||||
|
user management, and server administration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .db_fix import fix_database_schema
|
||||||
|
from .db_rebuild import rebuild_database
|
||||||
|
from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests
|
||||||
|
from .user_manager import list_users, create_user, reset_password, delete_user, create_admin_user
|
||||||
|
from .server import run_development_server
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Database utilities
|
||||||
|
'fix_database_schema',
|
||||||
|
'rebuild_database',
|
||||||
|
'test_database_connection',
|
||||||
|
'test_models',
|
||||||
|
'print_database_stats',
|
||||||
|
'run_all_tests',
|
||||||
|
|
||||||
|
# User management
|
||||||
|
'list_users',
|
||||||
|
'create_user',
|
||||||
|
'reset_password',
|
||||||
|
'delete_user',
|
||||||
|
'create_admin_user',
|
||||||
|
|
||||||
|
# Server management
|
||||||
|
'run_development_server',
|
||||||
|
]
|
||||||
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_fix.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_fix.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_rebuild.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_rebuild.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/db_test.cpython-311.pyc
Normal file
BIN
utils/__pycache__/db_test.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/server.cpython-311.pyc
Normal file
BIN
utils/__pycache__/server.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/user_manager.cpython-311.pyc
Normal file
BIN
utils/__pycache__/user_manager.cpython-311.pyc
Normal file
Binary file not shown.
78
utils/db_fix.py
Executable file
78
utils/db_fix.py
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Add the parent directory to path so we can import the app
|
||||||
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
from app import app, db_path
|
||||||
|
from models import db
|
||||||
|
|
||||||
|
def ensure_db_dir():
|
||||||
|
"""Make sure the database directory exists."""
|
||||||
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
|
||||||
|
def fix_database_schema():
|
||||||
|
"""Fix the database schema by adding missing columns."""
|
||||||
|
with app.app_context():
|
||||||
|
# Ensure directory exists
|
||||||
|
ensure_db_dir()
|
||||||
|
|
||||||
|
# Check if database exists, create tables if needed
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print("Database doesn't exist. Creating all tables from scratch...")
|
||||||
|
db.create_all()
|
||||||
|
print("Database tables created successfully!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Connect to existing database
|
||||||
|
print(f"Connecting to database: {db_path}")
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if User table exists
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user'")
|
||||||
|
if not cursor.fetchone():
|
||||||
|
print("User table doesn't exist. Creating all tables from scratch...")
|
||||||
|
conn.close()
|
||||||
|
db.create_all()
|
||||||
|
print("Database tables created successfully!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check existing columns
|
||||||
|
cursor.execute("PRAGMA table_info(user)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
column_names = [col[1] for col in columns]
|
||||||
|
print("Existing columns in User table:", column_names)
|
||||||
|
|
||||||
|
# Add missing columns
|
||||||
|
if 'created_at' not in column_names:
|
||||||
|
print("Adding 'created_at' column to User table...")
|
||||||
|
cursor.execute("ALTER TABLE user ADD COLUMN created_at TIMESTAMP")
|
||||||
|
|
||||||
|
if 'last_login' not in column_names:
|
||||||
|
print("Adding 'last_login' column to User table...")
|
||||||
|
cursor.execute("ALTER TABLE user ADD COLUMN last_login TIMESTAMP")
|
||||||
|
|
||||||
|
if 'avatar' not in column_names:
|
||||||
|
print("Adding 'avatar' column to User table...")
|
||||||
|
cursor.execute("ALTER TABLE user ADD COLUMN avatar VARCHAR(200)")
|
||||||
|
|
||||||
|
if 'bio' not in column_names:
|
||||||
|
print("Adding 'bio' column to User table...")
|
||||||
|
cursor.execute("ALTER TABLE user ADD COLUMN bio TEXT")
|
||||||
|
|
||||||
|
# Commit the changes
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("Database schema updated successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fix_database_schema()
|
||||||
81
utils/db_rebuild.py
Executable file
81
utils/db_rebuild.py
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
# Add the parent directory to path so we can import the app
|
||||||
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
from app import app, db_path, create_default_categories
|
||||||
|
from models import db, User, Category
|
||||||
|
|
||||||
|
def rebuild_database():
|
||||||
|
"""Completely rebuilds the database by dropping and recreating all tables."""
|
||||||
|
with app.app_context():
|
||||||
|
print(f"Database path: {db_path}")
|
||||||
|
|
||||||
|
# Back up existing database if it exists
|
||||||
|
if os.path.exists(db_path):
|
||||||
|
backup_path = db_path + '.backup'
|
||||||
|
try:
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(db_path, backup_path)
|
||||||
|
print(f"Backed up existing database to {backup_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not create backup - {str(e)}")
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
|
||||||
|
# Drop all tables and recreate them
|
||||||
|
print("Dropping all tables...")
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
print("Creating all tables...")
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Create admin user
|
||||||
|
print("Creating admin user...")
|
||||||
|
admin = User(
|
||||||
|
username='admin',
|
||||||
|
email='admin@example.com',
|
||||||
|
is_admin=True,
|
||||||
|
created_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
admin.set_password('admin')
|
||||||
|
db.session.add(admin)
|
||||||
|
|
||||||
|
# Create regular user
|
||||||
|
print("Creating regular user...")
|
||||||
|
user = User(
|
||||||
|
username='user',
|
||||||
|
email='user@example.com',
|
||||||
|
is_admin=False,
|
||||||
|
created_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
user.set_password('user')
|
||||||
|
db.session.add(user)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Commit to generate user IDs
|
||||||
|
db.session.commit()
|
||||||
|
print("Users created successfully!")
|
||||||
|
|
||||||
|
# Create default categories
|
||||||
|
print("Creating default categories...")
|
||||||
|
create_default_categories()
|
||||||
|
|
||||||
|
print("Database rebuild completed successfully!")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Error during database rebuild: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
rebuild_database()
|
||||||
120
utils/db_test.py
Executable file
120
utils/db_test.py
Executable file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Add the parent directory to path so we can import the app
|
||||||
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
from app import app, db_path
|
||||||
|
from models import db, User, Thought, MindMapNode, Category
|
||||||
|
|
||||||
|
def test_database_connection():
|
||||||
|
"""Test if the database exists and can be connected to."""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print(f"Database file does not exist: {db_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("PRAGMA integrity_check")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if result and result[0] == "ok":
|
||||||
|
print(f"Database integrity check passed: {db_path}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Database integrity check failed: {result}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error testing database connection: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_models():
|
||||||
|
"""Test if all models are properly defined and can be queried."""
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
print("\nTesting User model...")
|
||||||
|
user_count = User.query.count()
|
||||||
|
print(f" Found {user_count} users")
|
||||||
|
|
||||||
|
print("\nTesting Category model...")
|
||||||
|
category_count = Category.query.count()
|
||||||
|
print(f" Found {category_count} categories")
|
||||||
|
|
||||||
|
print("\nTesting MindMapNode model...")
|
||||||
|
node_count = MindMapNode.query.count()
|
||||||
|
print(f" Found {node_count} mindmap nodes")
|
||||||
|
|
||||||
|
print("\nTesting Thought model...")
|
||||||
|
thought_count = Thought.query.count()
|
||||||
|
print(f" Found {thought_count} thoughts")
|
||||||
|
|
||||||
|
if user_count == 0:
|
||||||
|
print("\nWARNING: No users found in the database. You might need to create an admin user.")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error testing models: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def print_database_stats():
|
||||||
|
"""Print database statistics."""
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
stats = []
|
||||||
|
stats.append(("Users", User.query.count()))
|
||||||
|
stats.append(("Categories", Category.query.count()))
|
||||||
|
stats.append(("Mindmap Nodes", MindMapNode.query.count()))
|
||||||
|
stats.append(("Thoughts", Thought.query.count()))
|
||||||
|
|
||||||
|
print("\nDatabase Statistics:")
|
||||||
|
print("-" * 40)
|
||||||
|
for name, count in stats:
|
||||||
|
print(f"{name:<20} : {count}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating database statistics: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all database tests."""
|
||||||
|
success = True
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("STARTING DATABASE TESTS")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Test database connection
|
||||||
|
print("\n1. Testing database connection...")
|
||||||
|
if not test_database_connection():
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# Test models
|
||||||
|
print("\n2. Testing database models...")
|
||||||
|
if not test_models():
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# Print statistics
|
||||||
|
print("\n3. Database statistics:")
|
||||||
|
if not print_database_stats():
|
||||||
|
success = False
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
if success:
|
||||||
|
print("All database tests completed successfully!")
|
||||||
|
else:
|
||||||
|
print("Some database tests failed. Check the output above for details.")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_all_tests()
|
||||||
34
utils/server.py
Executable file
34
utils/server.py
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the parent directory to path so we can import the app
|
||||||
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
def run_development_server(host='127.0.0.1', port=5000, debug=True):
|
||||||
|
"""Run the Flask development server."""
|
||||||
|
try:
|
||||||
|
print(f"Starting development server on http://{host}:{port}")
|
||||||
|
print("Press CTRL+C to stop the server")
|
||||||
|
app.run(host=host, port=port, debug=debug)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error starting development server: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Run the development server')
|
||||||
|
parser.add_argument('--host', default='127.0.0.1', help='Host to bind to')
|
||||||
|
parser.add_argument('--port', type=int, default=5000, help='Port to bind to')
|
||||||
|
parser.add_argument('--debug', action='store_true', default=True, help='Enable debug mode')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
run_development_server(host=args.host, port=args.port, debug=args.debug)
|
||||||
159
utils/user_manager.py
Executable file
159
utils/user_manager.py
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add the parent directory to path so we can import the app
|
||||||
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
from models import db, User
|
||||||
|
|
||||||
|
def list_users():
|
||||||
|
"""List all users in the database."""
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
users = User.query.all()
|
||||||
|
if not users:
|
||||||
|
print("No users found in the database.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
print("Found {} users:".format(len(users)))
|
||||||
|
print("-" * 60)
|
||||||
|
print("{:<5} {:<20} {:<30} {:<10}".format("ID", "Username", "Email", "Admin"))
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
print("{:<5} {:<20} {:<30} {:<10}".format(
|
||||||
|
user.id, user.username, user.email, "Yes" if user.is_admin else "No"
|
||||||
|
))
|
||||||
|
return users
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error listing users: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def create_user(username, email, password, is_admin=False):
|
||||||
|
"""Create a new user in the database."""
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
# Check if user already exists
|
||||||
|
existing_user = User.query.filter_by(username=username).first()
|
||||||
|
if existing_user:
|
||||||
|
print(f"User with username '{username}' already exists.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if email already exists
|
||||||
|
existing_email = User.query.filter_by(email=email).first()
|
||||||
|
if existing_email:
|
||||||
|
print(f"User with email '{email}' already exists.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create new user
|
||||||
|
user = User(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
is_admin=is_admin,
|
||||||
|
created_at=datetime.utcnow()
|
||||||
|
)
|
||||||
|
user.set_password(password)
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print(f"User '{username}' created successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Error creating user: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reset_password(username, new_password):
|
||||||
|
"""Reset password for a user."""
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if not user:
|
||||||
|
print(f"User '{username}' not found.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
user.set_password(new_password)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print(f"Password for user '{username}' reset successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Error resetting password: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_user(username):
|
||||||
|
"""Delete a user from the database."""
|
||||||
|
with app.app_context():
|
||||||
|
try:
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if not user:
|
||||||
|
print(f"User '{username}' not found.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print(f"User '{username}' deleted successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
print(f"Error deleting user: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_admin_user():
|
||||||
|
"""Create an admin user in the database."""
|
||||||
|
return create_user('admin', 'admin@example.com', 'admin', is_admin=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='User management utility')
|
||||||
|
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
|
||||||
|
|
||||||
|
# List users command
|
||||||
|
list_parser = subparsers.add_parser('list', help='List all users')
|
||||||
|
|
||||||
|
# Create user command
|
||||||
|
create_parser = subparsers.add_parser('create', help='Create a new user')
|
||||||
|
create_parser.add_argument('--username', '-u', required=True, help='Username')
|
||||||
|
create_parser.add_argument('--email', '-e', required=True, help='Email address')
|
||||||
|
create_parser.add_argument('--password', '-p', required=True, help='Password')
|
||||||
|
create_parser.add_argument('--admin', '-a', action='store_true', help='Make user an admin')
|
||||||
|
|
||||||
|
# Reset password command
|
||||||
|
reset_parser = subparsers.add_parser('reset-password', help='Reset a user password')
|
||||||
|
reset_parser.add_argument('--username', '-u', required=True, help='Username')
|
||||||
|
reset_parser.add_argument('--password', '-p', required=True, help='New password')
|
||||||
|
|
||||||
|
# Delete user command
|
||||||
|
delete_parser = subparsers.add_parser('delete', help='Delete a user')
|
||||||
|
delete_parser.add_argument('--username', '-u', required=True, help='Username to delete')
|
||||||
|
|
||||||
|
# Create admin command (shortcut)
|
||||||
|
admin_parser = subparsers.add_parser('create-admin', help='Create the default admin user')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == 'list':
|
||||||
|
list_users()
|
||||||
|
elif args.command == 'create':
|
||||||
|
create_user(args.username, args.email, args.password, args.admin)
|
||||||
|
elif args.command == 'reset-password':
|
||||||
|
reset_password(args.username, args.password)
|
||||||
|
elif args.command == 'delete':
|
||||||
|
delete_user(args.username)
|
||||||
|
elif args.command == 'create-admin':
|
||||||
|
create_admin_user()
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
Binary file not shown.
Binary file not shown.
264
website/app.py
264
website/app.py
@@ -1,264 +0,0 @@
|
|||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
|
||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
|
||||||
import json
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key')
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///mindmap.db'
|
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
login_manager = LoginManager(app)
|
|
||||||
login_manager.login_view = 'login'
|
|
||||||
|
|
||||||
# Database Models
|
|
||||||
class User(UserMixin, db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
|
||||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
|
||||||
password_hash = db.Column(db.String(128))
|
|
||||||
is_admin = db.Column(db.Boolean, default=False)
|
|
||||||
thoughts = db.relationship('Thought', backref='author', lazy=True)
|
|
||||||
|
|
||||||
def set_password(self, password):
|
|
||||||
self.password_hash = generate_password_hash(password)
|
|
||||||
|
|
||||||
def check_password(self, password):
|
|
||||||
return check_password_hash(self.password_hash, password)
|
|
||||||
|
|
||||||
class Thought(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
content = db.Column(db.Text, nullable=False)
|
|
||||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
branch = db.Column(db.String(100), nullable=False)
|
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
|
||||||
comments = db.relationship('Comment', backref='thought', lazy=True, cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
class Comment(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
content = db.Column(db.Text, nullable=False)
|
|
||||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=False)
|
|
||||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
|
||||||
author = db.relationship('User', backref='comments')
|
|
||||||
|
|
||||||
class MindMapNode(db.Model):
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
name = db.Column(db.String(100), nullable=False)
|
|
||||||
parent_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
|
|
||||||
children = db.relationship('MindMapNode', backref=db.backref('parent', remote_side=[id]))
|
|
||||||
thoughts = db.relationship('Thought', secondary='node_thought_association', backref='nodes')
|
|
||||||
|
|
||||||
# Association table for many-to-many relationship between MindMapNode and Thought
|
|
||||||
node_thought_association = db.Table('node_thought_association',
|
|
||||||
db.Column('node_id', db.Integer, db.ForeignKey('mind_map_node.id'), primary_key=True),
|
|
||||||
db.Column('thought_id', db.Integer, db.ForeignKey('thought.id'), primary_key=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
|
||||||
def load_user(id):
|
|
||||||
return User.query.get(int(id))
|
|
||||||
|
|
||||||
# Routes for authentication
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
|
||||||
def login():
|
|
||||||
if request.method == 'POST':
|
|
||||||
username = request.form.get('username')
|
|
||||||
password = request.form.get('password')
|
|
||||||
|
|
||||||
user = User.query.filter_by(username=username).first()
|
|
||||||
if user and user.check_password(password):
|
|
||||||
login_user(user)
|
|
||||||
next_page = request.args.get('next')
|
|
||||||
return redirect(next_page or url_for('index'))
|
|
||||||
flash('Ungültiger Benutzername oder Passwort')
|
|
||||||
return render_template('login.html')
|
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
|
||||||
def register():
|
|
||||||
if request.method == 'POST':
|
|
||||||
username = request.form.get('username')
|
|
||||||
email = request.form.get('email')
|
|
||||||
password = request.form.get('password')
|
|
||||||
|
|
||||||
if User.query.filter_by(username=username).first():
|
|
||||||
flash('Benutzername existiert bereits')
|
|
||||||
return redirect(url_for('register'))
|
|
||||||
|
|
||||||
if User.query.filter_by(email=email).first():
|
|
||||||
flash('E-Mail ist bereits registriert')
|
|
||||||
return redirect(url_for('register'))
|
|
||||||
|
|
||||||
user = User(username=username, email=email)
|
|
||||||
user.set_password(password)
|
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
login_user(user)
|
|
||||||
return redirect(url_for('index'))
|
|
||||||
return render_template('register.html')
|
|
||||||
|
|
||||||
@app.route('/logout')
|
|
||||||
@login_required
|
|
||||||
def logout():
|
|
||||||
logout_user()
|
|
||||||
return redirect(url_for('index'))
|
|
||||||
|
|
||||||
# Route for the homepage
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template('index.html')
|
|
||||||
|
|
||||||
# Route for the mindmap page
|
|
||||||
@app.route('/mindmap')
|
|
||||||
def mindmap():
|
|
||||||
return render_template('mindmap.html')
|
|
||||||
|
|
||||||
# Route for user profile
|
|
||||||
@app.route('/profile')
|
|
||||||
@login_required
|
|
||||||
def profile():
|
|
||||||
thoughts = Thought.query.filter_by(user_id=current_user.id).order_by(Thought.timestamp.desc()).all()
|
|
||||||
return render_template('profile.html', thoughts=thoughts)
|
|
||||||
|
|
||||||
# API routes for mindmap and thoughts
|
|
||||||
@app.route('/api/mindmap')
|
|
||||||
def get_mindmap():
|
|
||||||
root_nodes = MindMapNode.query.filter_by(parent_id=None).all()
|
|
||||||
|
|
||||||
def build_tree(node):
|
|
||||||
return {
|
|
||||||
'id': node.id,
|
|
||||||
'name': node.name,
|
|
||||||
'children': [build_tree(child) for child in node.children]
|
|
||||||
}
|
|
||||||
|
|
||||||
result = [build_tree(node) for node in root_nodes]
|
|
||||||
return jsonify(result)
|
|
||||||
|
|
||||||
@app.route('/api/thoughts/<int:node_id>', methods=['GET'])
|
|
||||||
def get_thoughts(node_id):
|
|
||||||
node = MindMapNode.query.get_or_404(node_id)
|
|
||||||
thoughts = []
|
|
||||||
|
|
||||||
for thought in node.thoughts:
|
|
||||||
thoughts.append({
|
|
||||||
'id': thought.id,
|
|
||||||
'content': thought.content,
|
|
||||||
'author': thought.author.username,
|
|
||||||
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
|
|
||||||
'comments_count': len(thought.comments),
|
|
||||||
'branch': thought.branch
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify(thoughts)
|
|
||||||
|
|
||||||
@app.route('/api/thought/<int:thought_id>', methods=['GET'])
|
|
||||||
def get_thought(thought_id):
|
|
||||||
thought = Thought.query.get_or_404(thought_id)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'id': thought.id,
|
|
||||||
'content': thought.content,
|
|
||||||
'author': thought.author.username,
|
|
||||||
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
|
|
||||||
'branch': thought.branch,
|
|
||||||
'comments_count': len(thought.comments)
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/api/thoughts', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def add_thought():
|
|
||||||
data = request.json
|
|
||||||
node_id = data.get('node_id')
|
|
||||||
content = data.get('content')
|
|
||||||
|
|
||||||
if not node_id or not content:
|
|
||||||
return jsonify({'error': 'Fehlende Daten'}), 400
|
|
||||||
|
|
||||||
node = MindMapNode.query.get_or_404(node_id)
|
|
||||||
|
|
||||||
thought = Thought(
|
|
||||||
content=content,
|
|
||||||
branch=node.name,
|
|
||||||
user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(thought)
|
|
||||||
node.thoughts.append(thought)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'id': thought.id,
|
|
||||||
'content': thought.content,
|
|
||||||
'author': thought.author.username,
|
|
||||||
'timestamp': thought.timestamp.strftime('%d.%m.%Y, %H:%M'),
|
|
||||||
'branch': thought.branch
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route('/api/comments/<int:thought_id>', methods=['GET'])
|
|
||||||
def get_comments(thought_id):
|
|
||||||
thought = Thought.query.get_or_404(thought_id)
|
|
||||||
comments = [
|
|
||||||
{
|
|
||||||
'id': comment.id,
|
|
||||||
'content': comment.content,
|
|
||||||
'author': comment.author.username,
|
|
||||||
'timestamp': comment.timestamp.strftime('%d.%m.%Y, %H:%M')
|
|
||||||
}
|
|
||||||
for comment in thought.comments
|
|
||||||
]
|
|
||||||
return jsonify(comments)
|
|
||||||
|
|
||||||
@app.route('/api/comments', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def add_comment():
|
|
||||||
data = request.json
|
|
||||||
thought_id = data.get('thought_id')
|
|
||||||
content = data.get('content')
|
|
||||||
|
|
||||||
if not thought_id or not content:
|
|
||||||
return jsonify({'error': 'Fehlende Daten'}), 400
|
|
||||||
|
|
||||||
thought = Thought.query.get_or_404(thought_id)
|
|
||||||
|
|
||||||
comment = Comment(
|
|
||||||
content=content,
|
|
||||||
thought_id=thought_id,
|
|
||||||
user_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(comment)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'id': comment.id,
|
|
||||||
'content': comment.content,
|
|
||||||
'author': comment.author.username,
|
|
||||||
'timestamp': comment.timestamp.strftime('%d.%m.%Y, %H:%M')
|
|
||||||
})
|
|
||||||
|
|
||||||
# Admin routes
|
|
||||||
@app.route('/admin')
|
|
||||||
@login_required
|
|
||||||
def admin():
|
|
||||||
if not current_user.is_admin:
|
|
||||||
flash('Zugriff verweigert')
|
|
||||||
return redirect(url_for('index'))
|
|
||||||
|
|
||||||
users = User.query.all()
|
|
||||||
nodes = MindMapNode.query.all()
|
|
||||||
thoughts = Thought.query.all()
|
|
||||||
|
|
||||||
return render_template('admin.html', users=users, nodes=nodes, thoughts=thoughts)
|
|
||||||
|
|
||||||
# Flask starten
|
|
||||||
if __name__ == '__main__':
|
|
||||||
with app.app_context():
|
|
||||||
# Make sure tables exist
|
|
||||||
db.create_all()
|
|
||||||
app.run(host="0.0.0.0", debug=True)
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
from app import app, db, User, MindMapNode
|
|
||||||
|
|
||||||
def init_database():
|
|
||||||
"""Initialize the database with admin user and mindmap structure."""
|
|
||||||
with app.app_context():
|
|
||||||
# Create all tables
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
# Check if we already have users
|
|
||||||
if User.query.first() is None:
|
|
||||||
print("Creating admin user...")
|
|
||||||
# Create admin user
|
|
||||||
admin = User(username='admin', email='admin@example.com', is_admin=True)
|
|
||||||
admin.set_password('admin123')
|
|
||||||
db.session.add(admin)
|
|
||||||
|
|
||||||
# Create regular test user
|
|
||||||
test_user = User(username='test', email='test@example.com', is_admin=False)
|
|
||||||
test_user.set_password('test123')
|
|
||||||
db.session.add(test_user)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
print("Admin user created successfully!")
|
|
||||||
|
|
||||||
# Check if we already have mindmap nodes
|
|
||||||
if MindMapNode.query.first() is None:
|
|
||||||
print("Creating initial mindmap structure...")
|
|
||||||
# Create initial mindmap structure
|
|
||||||
root = MindMapNode(name="Wissenschaftliche Mindmap")
|
|
||||||
db.session.add(root)
|
|
||||||
|
|
||||||
# Level 1 nodes
|
|
||||||
node1 = MindMapNode(name="Naturwissenschaften", parent=root)
|
|
||||||
node2 = MindMapNode(name="Geisteswissenschaften", parent=root)
|
|
||||||
node3 = MindMapNode(name="Technologie", parent=root)
|
|
||||||
node4 = MindMapNode(name="Künste", parent=root)
|
|
||||||
db.session.add_all([node1, node2, node3, node4])
|
|
||||||
|
|
||||||
# Level 2 nodes - Naturwissenschaften
|
|
||||||
node1_1 = MindMapNode(name="Physik", parent=node1)
|
|
||||||
node1_2 = MindMapNode(name="Biologie", parent=node1)
|
|
||||||
node1_3 = MindMapNode(name="Chemie", parent=node1)
|
|
||||||
node1_4 = MindMapNode(name="Astronomie", parent=node1)
|
|
||||||
db.session.add_all([node1_1, node1_2, node1_3, node1_4])
|
|
||||||
|
|
||||||
# Level 2 nodes - Geisteswissenschaften
|
|
||||||
node2_1 = MindMapNode(name="Philosophie", parent=node2)
|
|
||||||
node2_2 = MindMapNode(name="Geschichte", parent=node2)
|
|
||||||
node2_3 = MindMapNode(name="Psychologie", parent=node2)
|
|
||||||
node2_4 = MindMapNode(name="Soziologie", parent=node2)
|
|
||||||
db.session.add_all([node2_1, node2_2, node2_3, node2_4])
|
|
||||||
|
|
||||||
# Level 2 nodes - Technologie
|
|
||||||
node3_1 = MindMapNode(name="Informatik", parent=node3)
|
|
||||||
node3_2 = MindMapNode(name="Biotechnologie", parent=node3)
|
|
||||||
node3_3 = MindMapNode(name="Künstliche Intelligenz", parent=node3)
|
|
||||||
node3_4 = MindMapNode(name="Energietechnik", parent=node3)
|
|
||||||
db.session.add_all([node3_1, node3_2, node3_3, node3_4])
|
|
||||||
|
|
||||||
# Level 2 nodes - Künste
|
|
||||||
node4_1 = MindMapNode(name="Bildende Kunst", parent=node4)
|
|
||||||
node4_2 = MindMapNode(name="Musik", parent=node4)
|
|
||||||
node4_3 = MindMapNode(name="Literatur", parent=node4)
|
|
||||||
node4_4 = MindMapNode(name="Film", parent=node4)
|
|
||||||
db.session.add_all([node4_1, node4_2, node4_3, node4_4])
|
|
||||||
|
|
||||||
# Level 3 nodes - a few examples
|
|
||||||
# Physik
|
|
||||||
MindMapNode(name="Quantenphysik", parent=node1_1)
|
|
||||||
MindMapNode(name="Relativitätstheorie", parent=node1_1)
|
|
||||||
|
|
||||||
# Informatik
|
|
||||||
MindMapNode(name="Maschinelles Lernen", parent=node3_1)
|
|
||||||
MindMapNode(name="Softwareentwicklung", parent=node3_1)
|
|
||||||
MindMapNode(name="Datenbanken", parent=node3_1)
|
|
||||||
|
|
||||||
# Commit changes
|
|
||||||
db.session.commit()
|
|
||||||
print("Mindmap structure created successfully!")
|
|
||||||
|
|
||||||
print("Database initialization complete.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
init_database()
|
|
||||||
print("You can now run the application with 'python app.py'")
|
|
||||||
print("Login with:")
|
|
||||||
print(" Admin: username=admin, password=admin123")
|
|
||||||
print(" User: username=test, password=test123")
|
|
||||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
|||||||
flask
|
|
||||||
flask-login
|
|
||||||
flask-wtf
|
|
||||||
email-validator
|
|
||||||
python-dotenv
|
|
||||||
flask-sqlalchemy
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import os
|
|
||||||
from init_db import init_database
|
|
||||||
from app import app
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Initialize the database first
|
|
||||||
init_database()
|
|
||||||
|
|
||||||
# Run the Flask application
|
|
||||||
app.run(host="0.0.0.0", debug=True)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Erstelle eine einfache Mindmap-Struktur mit D3.js
|
|
||||||
const data = {
|
|
||||||
name: "Wissenschaftliche Mindmap",
|
|
||||||
children: [
|
|
||||||
{ name: "Forschung", children: [{ name: "Theorie" }, { name: "Experimente" }] },
|
|
||||||
{ name: "Technologie", children: [{ name: "Datenbanken" }, { name: "Cloud Computing" }] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// D3.js-Setup für die Darstellung der Mindmap
|
|
||||||
const width = 800;
|
|
||||||
const height = 600;
|
|
||||||
const margin = 50;
|
|
||||||
|
|
||||||
const svg = d3.select("#mindmap")
|
|
||||||
.append("svg")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", height);
|
|
||||||
|
|
||||||
const root = d3.hierarchy(data);
|
|
||||||
const treeLayout = d3.tree().size([width - margin, height - margin]);
|
|
||||||
treeLayout(root);
|
|
||||||
|
|
||||||
const links = svg.selectAll(".link")
|
|
||||||
.data(root.links())
|
|
||||||
.enter()
|
|
||||||
.append("line")
|
|
||||||
.attr("class", "link")
|
|
||||||
.attr("x1", d => d.source.x + margin)
|
|
||||||
.attr("y1", d => d.source.y + margin)
|
|
||||||
.attr("x2", d => d.target.x + margin)
|
|
||||||
.attr("y2", d => d.target.y + margin)
|
|
||||||
.attr("stroke", "#2c3e50");
|
|
||||||
|
|
||||||
const nodes = svg.selectAll(".node")
|
|
||||||
.data(root.descendants())
|
|
||||||
.enter()
|
|
||||||
.append("g")
|
|
||||||
.attr("class", "node")
|
|
||||||
.attr("transform", d => `translate(${d.x + margin},${d.y + margin})`);
|
|
||||||
|
|
||||||
nodes.append("circle")
|
|
||||||
.attr("r", 20)
|
|
||||||
.attr("fill", "#3498db");
|
|
||||||
|
|
||||||
nodes.append("text")
|
|
||||||
.attr("dx", 25)
|
|
||||||
.attr("dy", 5)
|
|
||||||
.text(d => d.data.name);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/* Grundlegendes Styling für die Seite */
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styling für den Header */
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button für die Navigation */
|
|
||||||
button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #3498db;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Admin | Wissenschaftliche Mindmap{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<div class="glass p-8 mb-8">
|
|
||||||
<h1 class="text-3xl font-bold text-white mb-4">Admin Bereich</h1>
|
|
||||||
<p class="text-white/70">Verwalte Benutzer, Gedanken und die Mindmap-Struktur.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
||||||
<!-- Users Section -->
|
|
||||||
<div class="dark-glass p-6" x-data="{ tab: 'users' }">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h2 class="text-2xl font-bold text-white">Benutzer</h2>
|
|
||||||
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ users|length }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-y-auto max-h-[500px]">
|
|
||||||
<table class="w-full text-white/90">
|
|
||||||
<thead class="text-white/60 text-sm uppercase">
|
|
||||||
<tr>
|
|
||||||
<th class="text-left py-3">ID</th>
|
|
||||||
<th class="text-left py-3">Benutzername</th>
|
|
||||||
<th class="text-left py-3">Email</th>
|
|
||||||
<th class="text-left py-3">Rolle</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for user in users %}
|
|
||||||
<tr class="border-t border-white/10">
|
|
||||||
<td class="py-3">{{ user.id }}</td>
|
|
||||||
<td class="py-3">{{ user.username }}</td>
|
|
||||||
<td class="py-3">{{ user.email }}</td>
|
|
||||||
<td class="py-3">
|
|
||||||
{% if user.is_admin %}
|
|
||||||
<span class="bg-purple-600/70 text-white text-xs px-2 py-1 rounded">Admin</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="bg-blue-600/70 text-white text-xs px-2 py-1 rounded">Benutzer</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mindmap Nodes Section -->
|
|
||||||
<div class="dark-glass p-6">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h2 class="text-2xl font-bold text-white">Mindmap Struktur</h2>
|
|
||||||
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ nodes|length }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-y-auto max-h-[500px]">
|
|
||||||
<div class="space-y-3">
|
|
||||||
{% for node in nodes %}
|
|
||||||
<div class="glass p-3">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="font-medium">{{ node.name }}</span>
|
|
||||||
<span class="text-xs text-white/60">ID: {{ node.id }}</span>
|
|
||||||
</div>
|
|
||||||
{% if node.parent %}
|
|
||||||
<p class="text-sm text-white/60 mt-1">Eltern: {{ node.parent.name }}</p>
|
|
||||||
{% else %}
|
|
||||||
<p class="text-sm text-white/60 mt-1">Hauptknoten</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<button class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-4 py-2 rounded-lg transition-all duration-300 text-sm w-full">
|
|
||||||
Neuen Knoten hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thoughts Section -->
|
|
||||||
<div class="dark-glass p-6">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h2 class="text-2xl font-bold text-white">Gedanken</h2>
|
|
||||||
<span class="bg-indigo-600 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">{{ thoughts|length }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-y-auto max-h-[500px]">
|
|
||||||
<div class="space-y-3">
|
|
||||||
{% for thought in thoughts %}
|
|
||||||
<div class="glass p-3">
|
|
||||||
<div class="flex justify-between items-start">
|
|
||||||
<span class="inline-block px-2 py-0.5 text-xs text-white/70 bg-white/10 rounded-full mb-1">{{ thought.branch }}</span>
|
|
||||||
<span class="text-xs text-white/50">{{ thought.timestamp.strftime('%d.%m.%Y') }}</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-white mb-1 line-clamp-2">{{ thought.content }}</p>
|
|
||||||
<div class="flex justify-between items-center mt-2 text-xs">
|
|
||||||
<span class="text-white/60">Von: {{ thought.author.username }}</span>
|
|
||||||
<span class="text-white/60">{{ thought.comments|length }} Kommentar(e)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{% block title %}Wissenschaftliche Mindmap{% endblock %}</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
||||||
<script>
|
|
||||||
tailwind.config = {
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
primary: '#3b82f6',
|
|
||||||
secondary: '#10b981',
|
|
||||||
accent: '#8b5cf6',
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
sans: ['Poppins', 'sans-serif'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
.glass {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
||||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-glass {
|
|
||||||
background: rgba(17, 24, 39, 0.7);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: #050b14;
|
|
||||||
font-family: 'Poppins', sans-serif;
|
|
||||||
position: relative;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gradient-text {
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
background-image: linear-gradient(to right, #4f46e5, #8b5cf6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#background-container {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#background-container canvas {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% block extra_head %}{% endblock %}
|
|
||||||
</head>
|
|
||||||
<body class="antialiased text-gray-100">
|
|
||||||
<!-- Animated background container -->
|
|
||||||
<div id="background-container"></div>
|
|
||||||
|
|
||||||
<div class="min-h-screen">
|
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="glass px-4 py-3 mx-4 mt-4 flex justify-between items-center">
|
|
||||||
<a href="{{ url_for('index') }}" class="text-white text-xl font-bold">MindMap</a>
|
|
||||||
<div class="flex space-x-4">
|
|
||||||
<a href="{{ url_for('mindmap') }}" class="text-white hover:text-indigo-200 transition-colors">Mindmap</a>
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
<a href="{{ url_for('profile') }}" class="text-white hover:text-indigo-200 transition-colors">Profil</a>
|
|
||||||
{% if current_user.is_admin %}
|
|
||||||
<a href="{{ url_for('admin') }}" class="text-white hover:text-indigo-200 transition-colors">Admin</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ url_for('logout') }}" class="text-white hover:text-indigo-200 transition-colors">Abmelden</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url_for('login') }}" class="text-white hover:text-indigo-200 transition-colors">Anmelden</a>
|
|
||||||
<a href="{{ url_for('register') }}" class="text-white hover:text-indigo-200 transition-colors">Registrieren</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Flash messages -->
|
|
||||||
{% with messages = get_flashed_messages() %}
|
|
||||||
{% if messages %}
|
|
||||||
<div class="container mx-auto mt-4 px-4">
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="glass p-4 mb-4 text-white">
|
|
||||||
{{ message }}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<main class="container mx-auto p-4">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
||||||
<script src="{{ url_for('static', filename='background.js') }}"></script>
|
|
||||||
{% block scripts %}{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="flex flex-col items-center justify-center min-h-[80vh] text-center">
|
|
||||||
<div class="glass p-8 max-w-2xl w-full">
|
|
||||||
<h1 class="text-4xl font-bold text-white mb-4">Willkommen zur Wissenschafts-Mindmap</h1>
|
|
||||||
<p class="text-xl text-white/80 mb-8">Verknüpfe Wissen in neuronalen Strukturen und teile deine Gedanken mit der Community.</p>
|
|
||||||
|
|
||||||
<div class="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4 justify-center">
|
|
||||||
<a href="{{ url_for('mindmap') }}" class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300 transform hover:scale-105">
|
|
||||||
Starte die Mindmap
|
|
||||||
</a>
|
|
||||||
{% if not current_user.is_authenticated %}
|
|
||||||
<a href="{{ url_for('register') }}" class="glass hover:bg-white/20 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300 transform hover:scale-105">
|
|
||||||
Registrieren
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-4xl">
|
|
||||||
<div class="glass p-6 text-white">
|
|
||||||
<div class="text-3xl mb-2">🧠</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-2">Visualisiere Wissen</h3>
|
|
||||||
<p class="text-white/80">Erkenne Zusammenhänge zwischen verschiedenen Wissensgebieten durch intuitive Mindmaps.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glass p-6 text-white">
|
|
||||||
<div class="text-3xl mb-2">💡</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-2">Teile Gedanken</h3>
|
|
||||||
<p class="text-white/80">Füge deine eigenen Gedanken zu bestehenden Themen hinzu und bereichere die Community.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glass p-6 text-white">
|
|
||||||
<div class="text-3xl mb-2">🔄</div>
|
|
||||||
<h3 class="text-xl font-semibold mb-2">Interaktive Vernetzung</h3>
|
|
||||||
<p class="text-white/80">Beteilige dich an Diskussionen und sieh wie sich Ideen gemeinsam entwickeln.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Anmelden | Wissenschaftliche Mindmap{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="flex justify-center items-center min-h-[80vh]">
|
|
||||||
<div class="glass p-8 w-full max-w-md">
|
|
||||||
<h1 class="text-3xl font-bold text-white mb-6 text-center">Anmelden</h1>
|
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('login') }}" class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label for="username" class="block text-sm font-medium text-white mb-1">Benutzername</label>
|
|
||||||
<input type="text" id="username" name="username" required
|
|
||||||
class="w-full px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password" class="block text-sm font-medium text-white mb-1">Passwort</label>
|
|
||||||
<input type="password" id="password" name="password" required
|
|
||||||
class="w-full px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button type="submit"
|
|
||||||
class="w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300">
|
|
||||||
Anmelden
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-6 text-center">
|
|
||||||
<p class="text-white/70">
|
|
||||||
Noch kein Konto?
|
|
||||||
<a href="{{ url_for('register') }}" class="text-indigo-300 hover:text-white font-medium transition-colors">
|
|
||||||
Registrieren
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,692 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Mindmap | Wissenschaftliche Mindmap{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_head %}
|
|
||||||
<style>
|
|
||||||
.node {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node circle {
|
|
||||||
fill: rgba(255, 255, 255, 0.2);
|
|
||||||
stroke: white;
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node text {
|
|
||||||
font-size: 12px;
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node--selected circle {
|
|
||||||
fill: rgba(139, 92, 246, 0.6);
|
|
||||||
r: 25;
|
|
||||||
stroke: rgba(255, 255, 255, 0.8);
|
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
fill: none;
|
|
||||||
stroke: rgba(255, 255, 255, 0.3);
|
|
||||||
stroke-width: 1.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thoughts-panel {
|
|
||||||
transform: translateX(100%);
|
|
||||||
transition: transform 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thoughts-panel.open {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thoughts-container {
|
|
||||||
max-height: calc(100vh - 250px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar for the thoughts panel */
|
|
||||||
.thoughts-container::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thoughts-container::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thoughts-container::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thoughts-container::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tooltip */
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
color: white;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Node color coding by category */
|
|
||||||
.node-science circle {
|
|
||||||
fill: rgba(79, 70, 229, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-humanities circle {
|
|
||||||
fill: rgba(16, 185, 129, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-technology circle {
|
|
||||||
fill: rgba(139, 92, 246, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-arts circle {
|
|
||||||
fill: rgba(236, 72, 153, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add a glow effect on hover */
|
|
||||||
.node:hover circle {
|
|
||||||
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="flex flex-col lg:flex-row h-[calc(100vh-100px)]">
|
|
||||||
<!-- Main mindmap visualization area -->
|
|
||||||
<div class="flex-grow relative" id="mindmap-container">
|
|
||||||
<div class="absolute top-4 left-4 z-10">
|
|
||||||
<h1 class="text-2xl font-bold text-white glass px-4 py-2 inline-block">Wissenschaftliche Mindmap</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Zoom controls -->
|
|
||||||
<div class="absolute bottom-4 left-4 glass p-2 flex space-x-2 z-10">
|
|
||||||
<button id="zoom-in" class="w-8 h-8 flex items-center justify-center text-white hover:bg-white/20 rounded-full transition-colors">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button id="zoom-out" class="w-8 h-8 flex items-center justify-center text-white hover:bg-white/20 rounded-full transition-colors">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button id="reset-view" class="w-8 h-8 flex items-center justify-center text-white hover:bg-white/20 rounded-full transition-colors">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Legend -->
|
|
||||||
<div class="absolute top-4 right-4 glass p-3 z-10">
|
|
||||||
<h3 class="text-sm font-semibold text-white mb-2">Kategorien</h3>
|
|
||||||
<div class="space-y-1 text-xs">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="inline-block w-3 h-3 rounded-full bg-indigo-600/60 mr-2"></span>
|
|
||||||
<span class="text-white/90">Naturwissenschaften</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="inline-block w-3 h-3 rounded-full bg-green-500/60 mr-2"></span>
|
|
||||||
<span class="text-white/90">Geisteswissenschaften</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="inline-block w-3 h-3 rounded-full bg-purple-500/60 mr-2"></span>
|
|
||||||
<span class="text-white/90">Technologie</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="inline-block w-3 h-3 rounded-full bg-pink-500/60 mr-2"></span>
|
|
||||||
<span class="text-white/90">Künste</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<svg id="mindmap" class="w-full h-full"></svg>
|
|
||||||
<div id="tooltip" class="tooltip"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thoughts panel (hidden by default) -->
|
|
||||||
<div id="thoughts-panel" class="thoughts-panel fixed lg:relative right-0 top-0 lg:top-auto h-full lg:h-auto w-full sm:w-96 dark-glass z-20">
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h2 class="text-xl font-bold text-white" id="selected-node-title">Keine Auswahl</h2>
|
|
||||||
<button id="close-panel" class="text-white/70 hover:text-white lg:hidden">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
<div class="glass p-4 mb-6">
|
|
||||||
<h3 class="text-sm font-medium text-white/80 mb-2">Teile deinen Gedanken</h3>
|
|
||||||
<textarea id="thought-input" rows="3" placeholder="Was denkst du zu diesem Thema?"
|
|
||||||
class="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all resize-none"></textarea>
|
|
||||||
<button id="submit-thought"
|
|
||||||
class="mt-2 w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-medium px-4 py-2 rounded-lg transition-all">
|
|
||||||
Gedanken teilen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="glass p-4 mb-6 text-center">
|
|
||||||
<p class="text-white/80 mb-2">Melde dich an, um deine Gedanken zu teilen</p>
|
|
||||||
<a href="{{ url_for('login') }}" class="text-indigo-300 hover:text-white font-medium">Anmelden</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h3 class="text-lg font-medium text-white mb-3">Community Gedanken</h3>
|
|
||||||
<div id="thoughts-container" class="thoughts-container space-y-4">
|
|
||||||
<div class="text-center py-8 text-white/50">
|
|
||||||
<p>Wähle einen Knoten aus, um Gedanken zu sehen</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Comment Modal -->
|
|
||||||
<div id="commentModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 hidden">
|
|
||||||
<div class="dark-glass p-6 w-full max-w-lg">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h3 class="text-xl font-bold text-white" id="comment-modal-title">Kommentare</h3>
|
|
||||||
<button onclick="closeCommentModal()" class="text-white/70 hover:text-white">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glass p-4 mb-4">
|
|
||||||
<p id="comment-thought-content" class="text-white mb-2"></p>
|
|
||||||
<div class="flex justify-between items-center text-xs text-white/60">
|
|
||||||
<span id="comment-thought-author"></span>
|
|
||||||
<span id="comment-thought-time"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4 max-h-60 overflow-y-auto" id="comments-list">
|
|
||||||
<!-- Comments will be loaded here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if current_user.is_authenticated %}
|
|
||||||
<div class="mt-4">
|
|
||||||
<textarea id="comment-input" rows="2" placeholder="Füge einen Kommentar hinzu..."
|
|
||||||
class="w-full px-3 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all resize-none"></textarea>
|
|
||||||
<button id="submit-comment"
|
|
||||||
class="mt-2 w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-medium px-4 py-2 rounded-lg transition-all">
|
|
||||||
Kommentar hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="glass p-4 text-center">
|
|
||||||
<p class="text-white/80 mb-2">Melde dich an, um Kommentare zu hinterlassen</p>
|
|
||||||
<a href="{{ url_for('login') }}" class="text-indigo-300 hover:text-white font-medium">Anmelden</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
||||||
<script>
|
|
||||||
// Global variables
|
|
||||||
let selectedNode = null;
|
|
||||||
let currentThoughtId = null;
|
|
||||||
const width = document.getElementById('mindmap-container').clientWidth;
|
|
||||||
const height = document.getElementById('mindmap-container').clientHeight;
|
|
||||||
const nodeCategories = {
|
|
||||||
'Naturwissenschaften': 'node-science',
|
|
||||||
'Geisteswissenschaften': 'node-humanities',
|
|
||||||
'Technologie': 'node-technology',
|
|
||||||
'Künste': 'node-arts'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize D3 visualization
|
|
||||||
const svg = d3.select('#mindmap')
|
|
||||||
.attr('width', width)
|
|
||||||
.attr('height', height);
|
|
||||||
|
|
||||||
const g = svg.append('g');
|
|
||||||
|
|
||||||
// Zoom behavior
|
|
||||||
const zoom = d3.zoom()
|
|
||||||
.scaleExtent([0.3, 3])
|
|
||||||
.on('zoom', (event) => {
|
|
||||||
g.attr('transform', event.transform);
|
|
||||||
});
|
|
||||||
|
|
||||||
svg.call(zoom);
|
|
||||||
|
|
||||||
// Reset to initial view
|
|
||||||
function resetView() {
|
|
||||||
svg.transition().duration(750).call(
|
|
||||||
zoom.transform,
|
|
||||||
d3.zoomIdentity.translate(width / 2, height / 2).scale(0.8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize tooltip
|
|
||||||
const tooltip = d3.select('#tooltip');
|
|
||||||
|
|
||||||
// Load mindmap data
|
|
||||||
function loadMindmap() {
|
|
||||||
fetch('/api/mindmap')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
renderMindmap(data[0]); // Assuming first item is the root node
|
|
||||||
resetView();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading mindmap:', error);
|
|
||||||
alert('Fehler beim Laden der Mindmap. Bitte die Seite neu laden.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get node category
|
|
||||||
function getNodeCategory(nodeName, rootCategories) {
|
|
||||||
// Check if the node is one of the root categories
|
|
||||||
if (nodeCategories[nodeName]) {
|
|
||||||
return nodeCategories[nodeName];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check parent categories for sub-nodes
|
|
||||||
for (const category in rootCategories) {
|
|
||||||
if (rootCategories[category].includes(nodeName)) {
|
|
||||||
return nodeCategories[category];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process data to track categories
|
|
||||||
function processData(node, rootCategories = {}) {
|
|
||||||
// Initialize categories for root node
|
|
||||||
if (node.name in nodeCategories) {
|
|
||||||
rootCategories[node.name] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record all children of a category
|
|
||||||
if (node.children) {
|
|
||||||
node.children.forEach(child => {
|
|
||||||
// Add to parent category
|
|
||||||
for (const category in rootCategories) {
|
|
||||||
if (node.name === category || rootCategories[category].includes(node.name)) {
|
|
||||||
rootCategories[category].push(child.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Process recursively
|
|
||||||
processData(child, rootCategories);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootCategories;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the mindmap visualization
|
|
||||||
function renderMindmap(data) {
|
|
||||||
// Clear previous content
|
|
||||||
g.selectAll('*').remove();
|
|
||||||
|
|
||||||
// Process data to track categories
|
|
||||||
const rootCategories = processData(data);
|
|
||||||
|
|
||||||
// Create hierarchical layout
|
|
||||||
const root = d3.hierarchy(data);
|
|
||||||
|
|
||||||
// Create tree layout
|
|
||||||
const treeLayout = d3.tree()
|
|
||||||
.size([height - 100, width - 200])
|
|
||||||
.nodeSize([80, 200]);
|
|
||||||
|
|
||||||
treeLayout(root);
|
|
||||||
|
|
||||||
// Create links
|
|
||||||
const links = g.selectAll('.link')
|
|
||||||
.data(root.links())
|
|
||||||
.enter()
|
|
||||||
.append('path')
|
|
||||||
.attr('class', 'link')
|
|
||||||
.attr('d', d => {
|
|
||||||
return `M${d.source.y},${d.source.x}
|
|
||||||
C${(d.source.y + d.target.y) / 2},${d.source.x}
|
|
||||||
${(d.source.y + d.target.y) / 2},${d.target.x}
|
|
||||||
${d.target.y},${d.target.x}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create nodes
|
|
||||||
const nodes = g.selectAll('.node')
|
|
||||||
.data(root.descendants())
|
|
||||||
.enter()
|
|
||||||
.append('g')
|
|
||||||
.attr('class', d => {
|
|
||||||
const categoryClass = getNodeCategory(d.data.name, rootCategories);
|
|
||||||
return `node ${categoryClass} ${d.data.id === selectedNode ? 'node--selected' : ''}`;
|
|
||||||
})
|
|
||||||
.attr('transform', d => `translate(${d.y},${d.x})`)
|
|
||||||
.on('click', (event, d) => selectNode(d.data.id, d.data.name))
|
|
||||||
.on('mouseover', function(event, d) {
|
|
||||||
tooltip.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style('opacity', 0.9);
|
|
||||||
tooltip.html(d.data.name)
|
|
||||||
.style('left', (event.pageX + 10) + 'px')
|
|
||||||
.style('top', (event.pageY - 28) + 'px');
|
|
||||||
})
|
|
||||||
.on('mouseout', function(d) {
|
|
||||||
tooltip.transition()
|
|
||||||
.duration(500)
|
|
||||||
.style('opacity', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add circles to nodes
|
|
||||||
nodes.append('circle')
|
|
||||||
.attr('r', 20)
|
|
||||||
.attr('class', d => (d.depth === 0) ? 'root-node' : ''); // Special class for root node
|
|
||||||
|
|
||||||
// Add text labels
|
|
||||||
nodes.append('text')
|
|
||||||
.attr('dy', 30)
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.text(d => d.data.name)
|
|
||||||
.each(function(d) {
|
|
||||||
// Wrap text for long labels
|
|
||||||
const text = d3.select(this);
|
|
||||||
const words = d.data.name.split(/\s+/);
|
|
||||||
|
|
||||||
if (words.length > 1) {
|
|
||||||
text.text('');
|
|
||||||
|
|
||||||
for (let i = 0; i < words.length; i++) {
|
|
||||||
const tspan = text.append('tspan')
|
|
||||||
.attr('x', 0)
|
|
||||||
.attr('dy', i === 0 ? 30 : 15)
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.text(words[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add node count indicator (how many thoughts are associated)
|
|
||||||
nodes.each(function(d) {
|
|
||||||
const node = d3.select(this);
|
|
||||||
|
|
||||||
// Get thought count for this node (an API call would be needed)
|
|
||||||
fetch(`/api/thoughts/${d.data.id}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(thoughts => {
|
|
||||||
if (thoughts.length > 0) {
|
|
||||||
// Add a small indicator
|
|
||||||
node.append('circle')
|
|
||||||
.attr('r', 8)
|
|
||||||
.attr('cx', 20)
|
|
||||||
.attr('cy', -20)
|
|
||||||
.attr('fill', 'rgba(139, 92, 246, 0.9)');
|
|
||||||
|
|
||||||
node.append('text')
|
|
||||||
.attr('x', 20)
|
|
||||||
.attr('y', -16)
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.attr('fill', 'white')
|
|
||||||
.attr('font-size', '10px')
|
|
||||||
.text(thoughts.length);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.error(`Error loading thoughts for node ${d.data.id}:`, error));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle node selection
|
|
||||||
function selectNode(nodeId, nodeName) {
|
|
||||||
selectedNode = nodeId;
|
|
||||||
|
|
||||||
// Update selected node in visualization
|
|
||||||
g.selectAll('.node').classed('node--selected', d => d.data.id === nodeId);
|
|
||||||
|
|
||||||
// Update panel title
|
|
||||||
document.getElementById('selected-node-title').textContent = nodeName;
|
|
||||||
|
|
||||||
// Load thoughts for this node
|
|
||||||
loadThoughts(nodeId);
|
|
||||||
|
|
||||||
// Open the thoughts panel on mobile
|
|
||||||
document.getElementById('thoughts-panel').classList.add('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load thoughts for a node
|
|
||||||
function loadThoughts(nodeId) {
|
|
||||||
const thoughtsContainer = document.getElementById('thoughts-container');
|
|
||||||
thoughtsContainer.innerHTML = '<div class="text-center py-4"><div class="inline-block animate-spin rounded-full h-6 w-6 border-t-2 border-white"></div></div>';
|
|
||||||
|
|
||||||
fetch(`/api/thoughts/${nodeId}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(thoughts => {
|
|
||||||
thoughtsContainer.innerHTML = '';
|
|
||||||
|
|
||||||
if (thoughts.length === 0) {
|
|
||||||
thoughtsContainer.innerHTML = '<div class="text-center py-4 text-white/50"><p>Noch keine Gedanken zu diesem Thema</p></div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
thoughts.forEach(thought => {
|
|
||||||
const thoughtEl = document.createElement('div');
|
|
||||||
thoughtEl.className = 'glass p-4 hover:shadow-lg transition-all';
|
|
||||||
thoughtEl.innerHTML = `
|
|
||||||
<p class="text-white mb-2">${thought.content}</p>
|
|
||||||
<div class="flex justify-between items-center text-xs">
|
|
||||||
<span class="text-white/70">Von ${thought.author}</span>
|
|
||||||
<span class="text-white/50">${thought.timestamp}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-right">
|
|
||||||
<button class="text-indigo-300 hover:text-white text-sm" onclick="openCommentModal(${thought.id})">
|
|
||||||
${thought.comments_count} Kommentar(e) anzeigen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
thoughtsContainer.appendChild(thoughtEl);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading thoughts:', error);
|
|
||||||
thoughtsContainer.innerHTML = '<div class="text-center py-4 text-red-300"><p>Fehler beim Laden der Gedanken</p></div>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit a new thought
|
|
||||||
function submitThought() {
|
|
||||||
if (!selectedNode) {
|
|
||||||
alert('Bitte wähle zuerst einen Knoten aus.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thoughtInput = document.getElementById('thought-input');
|
|
||||||
const content = thoughtInput.value.trim();
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
alert('Bitte gib einen Gedanken ein.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/api/thoughts', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
node_id: selectedNode,
|
|
||||||
content: content
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
thoughtInput.value = '';
|
|
||||||
loadThoughts(selectedNode); // Reload thoughts
|
|
||||||
|
|
||||||
// Refresh node counts in visualization
|
|
||||||
loadMindmap();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error adding thought:', error);
|
|
||||||
alert('Fehler beim Hinzufügen des Gedankens.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open comment modal
|
|
||||||
function openCommentModal(thoughtId) {
|
|
||||||
currentThoughtId = thoughtId;
|
|
||||||
|
|
||||||
// Get thought details
|
|
||||||
fetch(`/api/thought/${thoughtId}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(thought => {
|
|
||||||
document.getElementById('comment-thought-content').textContent = thought.content;
|
|
||||||
document.getElementById('comment-thought-author').textContent = `Von ${thought.author}`;
|
|
||||||
document.getElementById('comment-thought-time').textContent = thought.timestamp;
|
|
||||||
|
|
||||||
// Get comments
|
|
||||||
return fetch(`/api/comments/${thoughtId}`);
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(comments => {
|
|
||||||
const commentsList = document.getElementById('comments-list');
|
|
||||||
commentsList.innerHTML = '';
|
|
||||||
|
|
||||||
if (comments.length === 0) {
|
|
||||||
commentsList.innerHTML = '<p class="text-center text-white/50 py-3">Keine Kommentare vorhanden</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
comments.forEach(comment => {
|
|
||||||
const commentEl = document.createElement('div');
|
|
||||||
commentEl.className = 'glass p-3 mb-2';
|
|
||||||
commentEl.innerHTML = `
|
|
||||||
<p class="text-white text-sm">${comment.content}</p>
|
|
||||||
<div class="flex justify-between items-center mt-1 text-xs">
|
|
||||||
<span class="text-white/70">${comment.author}</span>
|
|
||||||
<span class="text-white/50">${comment.timestamp}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
commentsList.appendChild(commentEl);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error loading comments:', error));
|
|
||||||
|
|
||||||
document.getElementById('commentModal').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close comment modal
|
|
||||||
function closeCommentModal() {
|
|
||||||
document.getElementById('commentModal').classList.add('hidden');
|
|
||||||
currentThoughtId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit a new comment
|
|
||||||
function submitComment() {
|
|
||||||
if (!currentThoughtId) return;
|
|
||||||
|
|
||||||
const commentInput = document.getElementById('comment-input');
|
|
||||||
const content = commentInput.value.trim();
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
alert('Bitte gib einen Kommentar ein.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/api/comments', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
thought_id: currentThoughtId,
|
|
||||||
content: content
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
commentInput.value = '';
|
|
||||||
|
|
||||||
// Refresh comments
|
|
||||||
fetch(`/api/comments/${currentThoughtId}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(comments => {
|
|
||||||
const commentsList = document.getElementById('comments-list');
|
|
||||||
commentsList.innerHTML = '';
|
|
||||||
|
|
||||||
comments.forEach(comment => {
|
|
||||||
const commentEl = document.createElement('div');
|
|
||||||
commentEl.className = 'glass p-3 mb-2';
|
|
||||||
commentEl.innerHTML = `
|
|
||||||
<p class="text-white text-sm">${comment.content}</p>
|
|
||||||
<div class="flex justify-between items-center mt-1 text-xs">
|
|
||||||
<span class="text-white/70">${comment.author}</span>
|
|
||||||
<span class="text-white/50">${comment.timestamp}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
commentsList.appendChild(commentEl);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error refreshing comments:', error));
|
|
||||||
|
|
||||||
// Refresh thoughts since comment count changed
|
|
||||||
if (selectedNode) {
|
|
||||||
loadThoughts(selectedNode);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error adding comment:', error);
|
|
||||||
alert('Fehler beim Hinzufügen des Kommentars.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Load mindmap on page load
|
|
||||||
loadMindmap();
|
|
||||||
|
|
||||||
// Submit thought
|
|
||||||
document.getElementById('submit-thought')?.addEventListener('click', submitThought);
|
|
||||||
|
|
||||||
// Submit comment
|
|
||||||
document.getElementById('submit-comment')?.addEventListener('click', submitComment);
|
|
||||||
|
|
||||||
// Close panel on mobile
|
|
||||||
document.getElementById('close-panel')?.addEventListener('click', function() {
|
|
||||||
document.getElementById('thoughts-panel').classList.remove('open');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Zoom controls
|
|
||||||
document.getElementById('zoom-in')?.addEventListener('click', function() {
|
|
||||||
svg.transition().duration(300).call(zoom.scaleBy, 1.3);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('zoom-out')?.addEventListener('click', function() {
|
|
||||||
svg.transition().duration(300).call(zoom.scaleBy, 0.7);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('reset-view')?.addEventListener('click', resetView);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Profil | Wissenschaftliche Mindmap{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<div class="glass p-8 mb-8">
|
|
||||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-3xl font-bold text-white">Hallo, {{ current_user.username }}</h1>
|
|
||||||
<p class="text-white/70 mt-1">{{ current_user.email }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 md:mt-0">
|
|
||||||
<a href="{{ url_for('mindmap') }}"
|
|
||||||
class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300 inline-block">
|
|
||||||
Zur Mindmap
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dark-glass p-8">
|
|
||||||
<h2 class="text-2xl font-bold text-white mb-6">Deine Gedanken</h2>
|
|
||||||
|
|
||||||
{% if thoughts %}
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{% for thought in thoughts %}
|
|
||||||
<div class="glass p-6 hover:shadow-lg transition-all" x-data="{ showActions: false }" @mouseenter="showActions = true" @mouseleave="showActions = false">
|
|
||||||
<div class="flex justify-between items-start">
|
|
||||||
<span class="inline-block px-3 py-1 text-xs text-white/70 bg-white/10 rounded-full mb-3">{{ thought.branch }}</span>
|
|
||||||
<span class="text-xs text-white/50">{{ thought.timestamp.strftime('%d.%m.%Y, %H:%M') }}</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-white mb-4 leading-relaxed">{{ thought.content }}</p>
|
|
||||||
|
|
||||||
<div class="flex justify-between items-center" x-show="showActions" x-transition.opacity>
|
|
||||||
<div class="text-xs text-white/70">
|
|
||||||
<span>{{ thought.comments|length }} Kommentar(e)</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="#" onclick="openThoughtDetails('{{ thought.id }}')" class="text-indigo-300 hover:text-white text-sm">Details anzeigen</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-12">
|
|
||||||
<p class="text-white/70 mb-4">Du hast noch keine Gedanken geteilt.</p>
|
|
||||||
<a href="{{ url_for('mindmap') }}" class="text-indigo-300 hover:text-white">Zur Mindmap gehen und mitmachen</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thought Detail Modal -->
|
|
||||||
<div id="thoughtModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 hidden">
|
|
||||||
<div class="dark-glass p-8 w-full max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
||||||
<div class="flex justify-between items-center mb-6">
|
|
||||||
<h3 class="text-2xl font-bold text-white" id="modalThoughtTitle">Gedanke Details</h3>
|
|
||||||
<button onclick="closeThoughtModal()" class="text-white/70 hover:text-white">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="modalContent" class="space-y-6">
|
|
||||||
<div class="glass p-4">
|
|
||||||
<div class="flex justify-between items-start mb-2">
|
|
||||||
<span class="inline-block px-3 py-1 text-xs text-white/70 bg-white/10 rounded-full" id="modalBranch"></span>
|
|
||||||
<span class="text-xs text-white/50" id="modalTimestamp"></span>
|
|
||||||
</div>
|
|
||||||
<p class="text-white" id="modalThoughtContent"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 class="text-lg font-medium text-white mb-3">Kommentare</h4>
|
|
||||||
<div id="commentsList" class="space-y-3"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
function openThoughtDetails(thoughtId) {
|
|
||||||
fetch(`/api/thoughts/${thoughtId}`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(thought => {
|
|
||||||
document.getElementById('modalThoughtTitle').textContent = `Gedanke von ${thought.author}`;
|
|
||||||
document.getElementById('modalBranch').textContent = thought.branch;
|
|
||||||
document.getElementById('modalTimestamp').textContent = thought.timestamp;
|
|
||||||
document.getElementById('modalThoughtContent').textContent = thought.content;
|
|
||||||
|
|
||||||
// Load comments
|
|
||||||
return fetch(`/api/comments/${thoughtId}`);
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(comments => {
|
|
||||||
const commentsList = document.getElementById('commentsList');
|
|
||||||
commentsList.innerHTML = '';
|
|
||||||
|
|
||||||
if (comments.length === 0) {
|
|
||||||
commentsList.innerHTML = '<p class="text-white/50">Keine Kommentare vorhanden</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
comments.forEach(comment => {
|
|
||||||
const commentEl = document.createElement('div');
|
|
||||||
commentEl.className = 'glass p-3';
|
|
||||||
commentEl.innerHTML = `
|
|
||||||
<div class="flex justify-between items-start mb-1">
|
|
||||||
<span class="text-sm font-medium text-white">${comment.author}</span>
|
|
||||||
<span class="text-xs text-white/50">${comment.timestamp}</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-white/90 text-sm">${comment.content}</p>
|
|
||||||
`;
|
|
||||||
commentsList.appendChild(commentEl);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error loading thought details:', error));
|
|
||||||
|
|
||||||
document.getElementById('thoughtModal').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeThoughtModal() {
|
|
||||||
document.getElementById('thoughtModal').classList.add('hidden');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Registrieren | Wissenschaftliche Mindmap{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="flex justify-center items-center min-h-[80vh]">
|
|
||||||
<div class="glass p-8 w-full max-w-md">
|
|
||||||
<h1 class="text-3xl font-bold text-white mb-6 text-center">Registrieren</h1>
|
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('register') }}" class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label for="username" class="block text-sm font-medium text-white mb-1">Benutzername</label>
|
|
||||||
<input type="text" id="username" name="username" required
|
|
||||||
class="w-full px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="email" class="block text-sm font-medium text-white mb-1">E-Mail</label>
|
|
||||||
<input type="email" id="email" name="email" required
|
|
||||||
class="w-full px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label for="password" class="block text-sm font-medium text-white mb-1">Passwort</label>
|
|
||||||
<input type="password" id="password" name="password" required
|
|
||||||
class="w-full px-4 py-2 rounded-lg bg-white/10 border border-white/20 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button type="submit"
|
|
||||||
class="w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300">
|
|
||||||
Registrieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-6 text-center">
|
|
||||||
<p class="text-white/70">
|
|
||||||
Bereits registriert?
|
|
||||||
<a href="{{ url_for('login') }}" class="text-indigo-300 hover:text-white font-medium transition-colors">
|
|
||||||
Anmelden
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
Reference in New Issue
Block a user