Compare commits

234 Commits

Author SHA1 Message Date
f0d13cae78 feat: update logging format in app.log for better readability 2025-06-01 15:14:48 +01:00
a60b5c31ca chore: update compiled Python files in __pycache__ directories 2025-06-01 14:33:16 +01:00
f5c2e70a11 feat: Implementierung von Benachrichtigungen und sozialen Funktionen; Hinzufügen von API-Endpunkten für Benachrichtigungen, Benutzer-Follows und soziale Interaktionen; Verbesserung des Logging-Systems zur besseren Nachverfolgbarkeit von Systemereignissen. 2025-05-28 22:08:56 +02:00
1f4394e9b6 fix: update log format in app.log for better readability 2025-05-22 12:55:28 +01:00
37c457ca3f Kategorien- Button Funktion 2025-05-22 12:15:50 +01:00
936d983cb3 style: update mindmap.css for improved layout and design consistency 2025-05-22 12:02:17 +01:00
9ed9adfeaf 🔧 fix: update database and log files for improved stability 2025-05-20 10:11:07 +01:00
9c1475844c Merge branch 'main' of https://git.clickcandit.com/marwinm/website 2025-05-20 09:21:54 +01:00
310d0af0d1 feat: update logging format in app.log for better readability 2025-05-20 09:20:49 +01:00
cab8d28aeb 🔧 chore: Protokollaktualisierungen zur Dokumentation des Anwendungsstarts in app.log; mehrere Startmeldungen hinzugefügt für verbesserte Nachverfolgbarkeit. 2025-05-17 18:33:06 +02:00
9dc44f94f6 feat: update mindmap.js for improved performance and functionality 2025-05-17 10:25:40 +01:00
5b9ae85453 feat: Erweiterung der Mindmap-Funktionalität durch Verbesserung der Ladeanimation, Fehlerbehandlung und CSS-Anpassungen; Protokollaktualisierungen zur Fehlerverfolgung und Optimierung der Benutzeroberfläche. 2025-05-16 21:02:09 +01:00
302d5213ef feat: Erweiterung der Mindmap-Funktionalität durch Hinzufügen von Hover-Events für Knoten und Verbesserung der Zoom-Steuerung; Info-Panel für Knotendetails implementiert. 2025-05-16 20:40:53 +01:00
bb3211ab3d feat: enhance mindmap functionality and update UI components 2025-05-16 20:35:01 +01:00
2a246ee063 feat: Verbesserung der Mindmap-Funktionalität durch Einführung von Unterseiten und Anpassung des Designs in update_mindmap.js; Protokollaktualisierungen und Cache-Optimierungen vorgenommen. 2025-05-16 20:29:48 +01:00
fc8861c73c wir haben unterkategorien 2025-05-16 20:14:51 +01:00
8c49e7396e feat: enhance mindmap update functionality in update_mindmap.js 2025-05-16 20:04:36 +01:00
8e3c81fd06 🔧 chore: update cached Python files and logs for better performance 2025-05-16 15:02:44 +01:00
f18d23cfea 🔧 fix: update compiled Python cache and log file for better performance 2025-05-16 14:04:21 +01:00
d3405a7031 chore: Aktualisierung der Mindmap-Funktionalität und Integration von CSS/JS-Templates 2025-05-14 13:58:51 +02:00
5793902e47 chore: Änderungen commited 2025-05-14 13:56:11 +02:00
e73ccd7e80 "Update mindmap template with Convention-Format: templates/templates/mindmap.html" 2025-05-14 13:53:57 +02:00
e6784b712d 🎉 feat: "Add Minducture mindmap CSS and JS template integration" 2025-05-14 13:51:16 +02:00
35b5f321d4 chore: Änderungen commited 2025-05-14 13:48:24 +02:00
b68f65cc76 "Update mindmap Mindmap functionality 2025-05-14 13:47:35 +02:00
3a2f721f63 chore: Änderungen commited 2025-05-14 13:44:48 +02:00
5933195196 "Refactor Mindate mindmap UI updates for mindmap controls and components" (feat) 2025-05-14 13:41:24 +02:00
beccfa25a6 chore: Änderungen commited 2025-05-14 13:37:29 +02:00
bc5cef3ba8 "feat: Implement mindmap control enhancements and controls updates" 2025-05-14 12:51:53 +02:00
b867af9c8b chore: Änderungen commited 2025-05-14 12:47:02 +02:00
ee04432a49 chore: Änderungen commited 2025-05-14 12:43:13 +02:00
bbcee7f610 chore: Änderungen commited 2025-05-14 12:39:23 +02:00
1eb47fc230 chore: Änderungen commited 2025-05-14 12:15:57 +02:00
2921c5a824 chore: Änderungen commited 2025-05-14 12:13:19 +02:00
c98e238841 chore: Änderungen commited 2025-05-14 12:07:57 +02:00
af30a208ca chore: Änderungen commited 2025-05-14 12:02:15 +02:00
2e2f35ccc1 chore: Änderungen commited 2025-05-14 11:58:31 +02:00
fd293e53e1 chore: Änderungen commited 2025-05-14 11:55:58 +02:00
2b19cb000b chore: Änderungen commited 2025-05-14 11:39:58 +02:00
3aefe6c5e6 chore: Änderungen commited 2025-05-14 11:23:33 +02:00
c7b87dc643 chore: Änderungen commited 2025-05-14 11:21:10 +02:00
dc96252013 feat: enhance mindmap functionality with improved error handling and editing features; update logging for better debugging insights 2025-05-12 20:48:26 +02:00
ab56f44ae9 feat: update mindmap styles and improve node and edge rendering logic for better visualization 2025-05-12 20:36:12 +02:00
61124f5266 🗑️ chore: remove unused database and routing scripts; update cached Python bytecode files in __pycache__ directories 2025-05-12 20:31:20 +02:00
fab8d10f03 🔧 chore: Aktualisierung der Protokollierung mit zusätzlichen 404-Fehlern und Anwendungsstartmeldungen in app.log; Aktualisierung der Bytecode-Datei in __pycache__ 2025-05-12 19:54:41 +01:00
dec30e4681 feat: update app logic and improve mindmap functionality 2025-05-12 19:27:18 +01:00
a1bd999c6a feat: update app logic and improve mindmap functionality 2025-05-12 18:56:49 +01:00
b1d33ce643 feat: update app logic and improve logging functionality 2025-05-12 18:26:33 +01:00
293f877017 fix: update log file and remove unnecessary bytecode cache 2025-05-12 17:56:27 +01:00
e86d0b0f90 feat: update logging format in app.log for better readability 2025-05-11 14:05:59 +01:00
059fd167d6 🔧 chore: update cached Python bytecode files in __pycache__ directories 2025-05-11 13:30:26 +01:00
256d38e140 chore: Aktualisierung der Protokollierung mit zusätzlichen Fehlern 404 und Anwendungsstartmeldungen in app.log 2025-05-11 03:12:59 +02:00
4b75489631 chore: Erweiterung der Protokollierung mit zusätzlichen Startmeldungen der Anwendung in app.log 2025-05-11 01:13:33 +02:00
cb95c78276 "Refactor logging and improved app logging000_feat: Update code refactoring for enhanced performance enhancement in app module" 2025-05-11 00:31:29 +02:00
00cb100467 chore: Änderungen commited 2025-05-11 00:25:50 +02:00
8c66461dc8 chore: Änderungen commited 2025-05-11 00:23:27 +02:00
566f84fc0c "Refactor code cache for app.pycache__/app.c python file (feat)" 2025-05-11 00:20:49 +02:00
07eae42ba3 chore: Änderungen commited 2025-05-11 00:16:30 +02:00
0a1bebd862 "Update log file path for Conventionalenhanced logging: Migrations" 2025-05-11 00:13:40 +02:00
59b79b3466 chore: Änderungen commited 2025-05-11 00:04:49 +02:00
6f5526b648 📝 "Ref (Feature) Log Rotation) Logs now implemented for log rotation logs in app. 2025-05-11 00:01:50 +02:00
21148f0c0e chore: Änderungen commited 2025-05-10 23:56:26 +02:00
ba6cac32a9 "Refactor app. Implementing new feature for user authentication (feat): Add API endpoint enhancement)" 2025-05-10 23:53:54 +02:00
be767e9f27 Fehlerprotokollierung aktualisiert: Mehrere 404-Fehler für nicht gefundene URLs hinzugefügt, einschließlich detaillierter Rückverfolgungen und Benutzerinformationen. Diese Änderungen verbessern die Nachverfolgbarkeit von Anwendungsfehlern und unterstützen die Fehlerbehebung. 2025-05-10 23:52:41 +02:00
6aaf073ffb "Update log file structure for better logging and log rotation" 2025-05-10 23:45:42 +02:00
b6080f96cf chore: Änderungen commited 2025-05-10 23:40:37 +02:00
9ebf4b7abd "Refactor log rotation: update logs/logs/app.log, purge rotated to simplify and deleted postcss 2025-05-10 23:40:18 +02:00
5d35983f15 chore: Änderungen commited 2025-05-10 23:36:28 +02:00
7278ece2b8 "feat: Add PostCSS 2025-05-10 23:36:23 +02:00
f677e98795 "Refactor database schema for user authentication and data migration" (feat: feat or fix) 2025-05-10 23:29:42 +02:00
40c3f6d9b4 📝 "Refactor: Update log rotation correction in logging update to app.log files 2025-05-10 23:26:14 +02:00
9939db731b chore: Änderungen commited 2025-05-10 23:23:32 +02:00
d0f32a8355 chore: Änderungen commited 2025-05-10 23:20:10 +02:00
02d1801fc9 "Refactor app log file path update" 2025-05-10 23:17:13 +02:00
c51a8e23ca chore: Aktualisierung der Codebasis und Verbesserung der Struktur für bessere Wartbarkeit 2025-05-10 23:15:45 +02:00
1600647bc4 chore: Änderungen commited 2025-05-10 23:15:08 +02:00
82d03f6c48 "Refactor app. Update import/Improve Python Cython code formatting (#__" 2025-05-10 23:12:51 +02:00
d1352286b7 chore: Änderungen commited 2025-05-10 23:10:31 +02:00
e7b3374c53 feat: Hinzufügen von CSS-Stilen für die Suchergebnisse und deren Darstellung in der Benutzer-Mindmap-Vorlage zur Verbesserung der Benutzeroberfläche 2025-05-10 23:05:44 +02:00
4bf046c657 feat: Hinzufügen einer Suchleiste und Import-/Export-Buttons zur Benutzer-Mindmap-Vorlage für verbesserte Benutzerinteraktion 2025-05-10 23:05:27 +02:00
892a1212d9 chore: Änderungen commited 2025-05-10 23:04:52 +02:00
8440b7c30d "Refactor database connection for improved data consistency (feat" 2025-05-10 23:01:22 +02:00
74c2783b1a "Refactor app. Update import statements and for improved usability improvements (feat(feat): Implementing)" 2025-05-10 22:58:56 +02:00
fcd82eb5c9 chore: Änderungen commited 2025-05-10 22:56:49 +02:00
c654986f65 "Refactor user mind map template updates for better user experience" (feat) 2025-05-10 22:54:31 +02:00
f4ab617c59 chore: Änderungen commited 2025-05-10 22:52:11 +02:00
9c36179f29 "Refactor UI refactoring: Simplify app.c-specific components 2025-05-10 22:43:13 +02:00
f292cf1ce5 chore: Änderungen commited 2025-05-10 22:35:14 +02:00
3a20ea0282 chore: Änderungen commited 2025-05-10 22:28:19 +02:00
44986bfa23 "feat: Refactor UI update for my_my_account_account template" 2025-05-10 22:24:33 +02:00
41195a44cb chore: Änderungen commited 2025-05-10 22:20:49 +02:00
e1cd23230d "Refactor database schema for user authentication system data" 2025-05-10 22:17:26 +02:00
77095e91b6 chore: Änderungen commited 2025-05-10 22:14:16 +02:00
6322e046c5 "Feature: Integrate app and script and related files for Mindmap 2025-05-10 22:11:43 +02:00
2584bae149 chore: aktualisiere kompilierte Python-Bytecode-Datei in __pycache__ 2025-05-10 21:59:18 +01:00
c0bd7a3986 feat: add systades.db and update __pycache__ for improved performance 2025-05-10 21:58:29 +01:00
dec4a57b89 feat: enhance mindmap update functionality in app.py and update_mindmap.js 2025-05-10 21:27:57 +01:00
6a3b3a81c1 feat: verbessere neuronale Netzwerkdarstellung und Mindmap-Layout durch Anpassungen an Farben, Größen und Animationen 2025-05-10 21:19:54 +01:00
629813c486 feat(app): Aktualisierung der Datenbankinitialisierung für Flask 2.2+ Kompatibilität und Verbesserung der Initialisierungslogik 2025-05-10 22:11:33 +02:00
7cb2bf1ed0 feat: enhance mindmap and neural network background functionality 2025-05-10 20:40:13 +01:00
ed1d41d316 chore: automatic commit 2025-05-10 18:18 2025-05-10 18:18:34 +01:00
fe3cf81bc7 "Add/update or fix?" 2025-05-10 18:09:53 +02:00
2e68ae30b8 "Update .env and migration files for database schema" 2025-05-10 18:07:49 +02:00
858fdf5c44 chore: Änderungen commited 2025-05-10 18:05:14 +02:00
4948f3ad2a "Refactor database schema for database connection and data integrity improvements" 2025-05-10 18:02:27 +02:00
52954e51f1 Merge branch 'main' of https://git.clickcandit.com/marwinm/website 2025-05-10 16:04:40 +01:00
14f1356551 chore: update compiled Python bytecode files in __pycache__ directories 2025-05-10 15:49:21 +01:00
44c7183e97 feat(profile): verbessere das Profil-Layout mit neuen Tab-Stilen, verbessere die Benutzeroberfläche für Profil- und Passwortaktualisierungen und füge Animationen für Tab-Inhalte hinzu. 2025-05-10 15:56:14 +02:00
d99cae4956 chore: Änderungen commited 2025-05-10 15:42:34 +02:00
3ae5f2527c Merge branches 'main' and 'main' of https://git.clickcandit.com/marwinm/website 2025-05-10 15:35:35 +02:00
412dabd5c1 feat(mindmap): füge Icons und Farben für Kategorien hinzu, verbessere das Layout und die Darstellung der Mindmap in update_mindmap.js 2025-05-09 20:18:45 +01:00
5ade301f80 noch nicht ganz 2025-05-09 20:06:35 +01:00
118f8ed132 feat: enhance mindmap update functionality in update_mindmap.js 2025-05-09 20:00:46 +01:00
121f46df01 feat(mindmap): erweitere die Funktionalität zur Handhabung von Unterthemen und verbessere die Benutzerinteraktion durch neue Navigations- und Layout-Elemente in update_mindmap.js 2025-05-09 19:58:03 +01:00
4b0613eb6b feat(mindmap): entferne veraltete Mindmap-Module und -Dateien zur Optimierung der Codebasis und Verbesserung der Wartbarkeit 2025-05-09 19:46:26 +01:00
dd172d8596 feat(mindmap): improve performance of mind map rendering logic 2025-05-09 19:28:00 +01:00
653b3abe91 ich geh schlafen 2025-05-06 22:50:04 +01:00
ec50886145 es wird spät 2025-05-06 22:47:08 +01:00
c888dcc452 feat: enhance mindmap update functionality in update_mindmap.js 2025-05-06 22:26:28 +01:00
acceec4352 hab glaube gefixed 2025-05-06 22:24:49 +01:00
f093a6211c ich bin dran ! 2025-05-06 22:12:46 +01:00
58a5ea00bd Mindmap wird nicht initialisiert ! :( 2025-05-06 21:58:27 +01:00
aeb829e36a feat(mindmap): enhance interaction and initialization logic in mindmap files 2025-05-06 21:53:54 +01:00
49e5e19b7c feat(mindmap): aktualisiere die Mindmap-Datenstruktur mit neuen Knoten und Kanten, um die Benutzerinteraktion zu verbessern. Füge dynamische Knotenbeschreibungen hinzu und implementiere eine Funktion zur Aktualisierung der Mindmap, die das Layout optimiert und die Benutzererfahrung verbessert. 2025-05-06 21:25:17 +01:00
903e095b66 feat: enhance mindmap functionality and improve user interaction 2025-05-06 21:23:29 +01:00
2d083f5c0a feat: update mindmap template for improved layout and usability 2025-05-06 20:52:51 +01:00
cbe8dc3bd0 "Refactor database schema for database operations and db maintenance utilities improvements" 2025-05-05 07:04:37 +02:00
7c1533c20d feat: improve mindmap interaction and enhance performance in JS files 2025-05-03 20:22:59 +01:00
c285b7d8dc feat(mindmap): verbessere die Benutzerinteraktion und das visuelle Design der Mindmap. Füge dynamische Knotenbeschreibungen hinzu, aktualisiere die Farbpalette und optimiere die Zoom- und Knoteninteraktionen. Erweitere die Seitenleisten mit neuen Panels und verbessere die Animationen für ein ansprechenderes Nutzererlebnis. 2025-05-03 19:57:28 +01:00
21ddd38e13 feat(mindmap): enhance functionality for better user interaction 2025-05-03 19:52:35 +01:00
1cf7bfbf76 feat: erweitere die Mindmap-API zur dynamischen Erstellung und Anzeige wissenschaftlicher Knoten sowie zur Verbesserung der Fehlerbehandlung. Füge neues Skript zur Aktualisierung der Mindmap hinzu. 2025-05-03 19:44:18 +01:00
40b28134fc feat: entferne nicht verwendete Forum-Funktionen aus app.py und aktualisiere die Basisvorlage für die Suchfunktion 2025-05-03 19:31:34 +01:00
d5fababd49 feat: update app logic and enhance base template rendering 2025-05-03 19:19:53 +01:00
7c742debdf 🔧 chore: passe die Konfiguration des neuronalen Netzwerks an, indem die Knotenanzahl auf 10 und die Clusteranzahl auf 7 erhöht wird, um die Verteilung zu optimieren. 2025-05-03 16:00:48 +01:00
4a4271a23c feat: aktualisiere das Benutzerprofil, um grundlegende Statistiken anzuzeigen und die Handhabung von Fehlern zu verbessern, während die Datenbankstruktur berücksichtigt wird. 2025-05-03 15:37:33 +01:00
c1038b479f feat: update app.py to improve performance and optimize resource usage 2025-05-03 15:34:49 +01:00
cd0083544a feat: update profile template for improved user experience 2025-05-03 14:59:49 +01:00
a03bec2dff chore: update database and remove unused compiled Python files 2025-05-03 13:37:07 +01:00
997479581d 🔧 chore: update compiled Python bytecode files in __pycache__ directories 2025-05-03 12:39:17 +01:00
8153390e35 Verbessere das Design der Profilseite: Aktualisiere CSS-Stile für eine verbesserte Benutzeroberfläche im Light Mode, einschließlich optimierter Hintergründe, Rahmen und Schattierungen für verschiedene Elemente wie Einstellungen, Statistiken und Interaktionselemente. 2025-05-02 19:56:25 +02:00
bfa155628e "Refactor UIKern:000:2 amend template.pycache__/app.00/app.c file modifications to enhance user experience in-0000usability." 2025-05-02 19:53:04 +02:00
700a8a3b89 chore: Änderungen commited 2025-05-02 19:46:47 +02:00
808481ffe7 chore: Änderungen commited 2025-05-02 19:44:34 +02:00
e2c8cfaacf "Refactor templating templates for better" 2025-05-02 19:41:56 +02:00
78e37fa717 chore: Änderungen commited 2025-05-02 19:39:44 +02:00
b2cf50626a "Add default avatar icon for avatar" 2025-05-02 19:26:55 +02:00
7f48526315 chore: Aktualisiere die kompilierte Python-Datei im __pycache__ Verzeichnis 2025-05-02 19:24:20 +02:00
84f8a6bf31 chore: Änderungen commited 2025-05-02 19:23:38 +02:00
7003c89447 "Refactor database schema for user profiles and profile templates updated to remove obsolete db table 'systades (feat): systades.db) system data)" 2025-05-02 19:21:19 +02:00
d0821db983 chore: Änderungen commited 2025-05-02 19:18:49 +02:00
f0c4c514c4 "Refactor Mindicate mindmap Update for Mindate Mindous Mind mindmap Modifications in the Mind (feat/update to-mendments toughly, update forums ofthe mind-peed_induced.cetapane 2025-05-02 19:16:34 +02:00
304a399b85 chore: Änderungen commited 2025-05-02 19:13:14 +02:00
a5396c0d6e 🎨 style: update base styles and add user mindmap template 2025-05-02 19:06:09 +02:00
9cc4e70cba 🎨 style: erweitere die CSS-Stile für Schaltflächen und Links im Basis-Template zur Verbesserung der Benutzeroberfläche 2025-05-02 19:01:47 +02:00
a8cac08d30 feat: enhance chatgpt assistant styling and functionality improvements 2025-05-02 18:59:13 +02:00
42a7485ce1 🎨 style: update CSS and remove unused JS for improved design consistency 2025-05-02 18:57:01 +02:00
54a5ccc224 🎨 style: update neural network background CSS for improved aesthetics 2025-05-02 18:54:33 +02:00
a99f82d4cf feat: add theme toggle functionality and update related files 2025-05-02 18:52:18 +02:00
699127f41f 🎨 style: update UI and database schema for improved user experience 2025-05-02 18:49:02 +02:00
e8d356a27a 🎨 style: update base styles and templates for improved layout and design 2025-05-02 18:46:05 +02:00
daf2704253 feat: update profile template and remove compiled Python file 2025-05-02 18:42:56 +02:00
084059449f 🎨 style: update profile template and improve app.py functionality 2025-05-02 18:38:59 +02:00
c9bbc6ff25 🎨 style: update styles and layout in base templates and CSS files 2025-05-02 18:33:41 +02:00
742e3fda20 feat: enhance UI and functionality for mindmap creation and profile pages 2025-05-02 18:31:25 +02:00
54aa246b79 feat: add create_mindmap template for mind mapping functionality 2025-05-02 18:28:54 +02:00
505fb9aa47 🔄 chore: aktualisiere die Bytecode-Dateien im __pycache__-Verzeichnis und die systades.db-Datenbankdatei 2025-05-02 18:27:26 +02:00
e4e6541b8c 🎉 feat: update app logic and database schema for improved performance 2025-05-02 18:22:02 +02:00
e724181915 feat: improve user management and update styles for better UX 2025-05-02 18:19:58 +02:00
460c3f987e 🎨 style: update base styles and database schema for improved layout 2025-05-02 18:14:04 +02:00
7f33dea278 feat(database): add new systades.db instance and update existing file 2025-05-02 18:11:57 +02:00
726d9c9c70 🎉 feat(database): add user fields and password column migrations 2025-05-02 18:09:33 +02:00
81170fbd3d 🎨 style: update base styles and background for improved UI consistency 2025-05-02 18:02:00 +02:00
eff3fda1ca chore: update Python bytecode files in __pycache__ directory 2025-05-02 17:54:46 +02:00
d49b266d96 User Profil Fix Versuch 1 2025-05-02 16:48:00 +01:00
34a08c4a6a feat: aktualisiere Favicon mit neuem Design und passe die HTML-Vorlage an, um das neue Favicon zu integrieren 2025-05-02 16:17:39 +01:00
7918de1723 feat: update favicon generation and add neuron favicon image 2025-05-02 16:07:46 +01:00
a0e4cd2208 feat: implementiere Mindmap-Funktionalität mit dynamischer Datenladung und verbesserten Benutzeroberflächen-Elementen in mindmap.html und mindmap-init.js 2025-05-02 09:27:22 +01:00
2199d6007c feat: verbessere das Layout und die Benutzeroberfläche des Chatbereichs in index.html mit neuen Stilen und verbesserten Eingabefeldern 2025-05-02 09:14:01 +01:00
7fb9452d09 feat: passe die Konfiguration des neuronalen Netzwerks an, um die Knotenanzahl zu reduzieren und die Clusteranzahl zu erhöhen 2025-05-02 09:07:02 +01:00
1f3e60efde feat: update index.html template for improved layout and accessibility 2025-05-02 09:04:48 +01:00
5e97381c8f nenn mich designer 2025-05-02 08:51:40 +01:00
4c402423c0 feat: verbessere die Logik des neuronalen Netzwerk-Hintergrunds mit optimierten Animationen und vereinfachter Struktur 2025-05-02 08:40:34 +01:00
6d2595e3a6 feat: update neural network background logic for improved performance 2025-05-02 08:33:45 +01:00
29b44e5c52 Community funktioniert nicht 2025-05-02 08:25:06 +01:00
693e542d5f UTF8 in .env, FLASK DEBUG 2025-05-02 08:06:38 +01:00
4c3e476338 feat: update environment config and add community preview template 2025-05-02 08:01:23 +01:00
613c38ccb2 🔧 chore: update environment variables in .env file 2025-05-02 07:30:58 +01:00
91fdd43fe0 🔒 chore: entferne den OpenAI API-Schlüssel aus der Beispielumgebung für Sicherheitsgründe 2025-05-01 21:15:04 +01:00
f36dd5ffaa OpenAI Api Key 2025-05-01 21:10:19 +01:00
2e1c3ce8b0 Community erstellt 2025-05-01 21:04:37 +01:00
d80c4c9aec feat(models): update model structure for improved data handling 2025-05-01 20:56:05 +01:00
3b0bea959c 🔧 fix: update venv configuration for improved compatibility 2025-05-01 20:25:01 +01:00
cb3bfe0e6a LOL 2025-05-01 19:57:26 +01:00
fd63810845 feat: update environment and scripts for improved functionality 2025-05-01 16:29:57 +02:00
883973fe7b feat: update environment variables and enhance mindmap functionality 2025-05-01 16:27:40 +02:00
027e632856 feat: update example.env with new configuration settings 2025-05-01 16:24:29 +02:00
406289e54f 🎨 feat(mindmap): improve rendering performance and optimize code structure 2025-05-01 16:16:26 +02:00
71b33e6cec feat(mindmap): enhance mindmap rendering performance and responsiveness 2025-05-01 16:14:14 +02:00
c74d3164bb 🎨 feat: update mindmap templates and JS module for improved UI design 2025-05-01 16:11:42 +02:00
4982cddeef 🎉 feat: update Dockerfile and scripts for improved functionality 2025-05-01 16:05:52 +02:00
631619ccb4 feat: update docker-compose.yml for improved service configuration 2025-05-01 16:01:00 +02:00
f9881b678d 🔄 chore: aktualisiere die .env-Datei für verbesserte Konfiguration 2025-05-01 11:27:11 +01:00
259ce3cf69 feat: update Dockerfile and docker-compose for improved build process 2025-05-01 11:24:27 +01:00
9f4743eaea feat: update cached Python files and add new static image asset 2025-05-01 10:55:30 +01:00
de0f837cfd Optimierung der Projektstruktur: Entferne nicht mehr benötigte Skripte und Dateien, um die Wartbarkeit zu erhöhen und veraltete Komponenten zu beseitigen. 2025-04-30 15:51:07 +02:00
1c49ddfb19 chore: automatic commit 2025-04-30 15:49 2025-04-30 15:49:18 +02:00
46c16e5f01 chore: automatic commit 2025-04-30 15:44 2025-04-30 15:44:02 +02:00
84667bca00 chore: automatic commit 2025-04-30 15:41 2025-04-30 15:41:00 +02:00
779449559d chore: automatic commit 2025-04-30 15:38 2025-04-30 15:38:56 +02:00
721a10e861 Entferne nicht mehr benötigte Skripte: Lösche die Dateien check_schema.py, create_default_users.py, fix_user_table.py, test_app.py und windows_setup.bat, um die Projektstruktur zu optimieren und veraltete Komponenten zu entfernen. 2025-04-30 15:33:39 +02:00
a431873ca2 chore: automatic commit 2025-04-30 15:29 2025-04-30 15:29:23 +02:00
e4ab1e1bb5 chore: automatic commit 2025-04-30 12:48 2025-04-30 12:48:06 +02:00
f69356473b Entferne nicht mehr benötigte Dateien: Lösche docker-compose.yml, Dockerfile, README.md, requirements.txt, start_server.bat, start-flask-server.py, start.sh, test_server.py, sowie alle zugehörigen Datenbank- und Website-Dateien. Diese Bereinigung optimiert die Projektstruktur und entfernt veraltete Komponenten. 2025-04-30 12:34:06 +02:00
38ac13e87c chore: automatic commit 2025-04-30 12:32 2025-04-30 12:32:36 +02:00
0afb8cb6e2 Update neural network background configuration: reduce node count and connection distance, adjust glow and node colors, and modify shadow blur for improved visual clarity and performance. 2025-04-29 20:58:27 +01:00
5d282d2108 Refactor neural network background animation: streamline the code by consolidating node and connection logic, enhancing visual effects with improved glow and animation dynamics. Introduce responsive canvas resizing and optimize particle behavior for a smoother experience. 2025-04-29 20:54:24 +01:00
4aba72efa2 Merge branch 'main' of https://git.clickcandit.com/marwinm/website 2025-04-29 20:52:11 +01:00
89476d5353 w 2025-04-29 20:51:49 +01:00
0f7a33340a Update mindmap database: replace binary file with a new version to reflect recent changes in structure and data. 2025-04-27 08:56:56 +01:00
73501e7cda Add Flask server startup scripts: introduce start_server.bat for Windows and start-flask-server.py for enhanced server management. Update run.py to include logging and threaded request handling. Add test_server.py for server accessibility testing. 2025-04-25 17:09:09 +01:00
9f8eba6736 Refactor database initialization: streamline the process by removing the old init_database function, implementing a new structure for database setup, and ensuring the creation of a comprehensive mindmap hierarchy with an admin user. Update app.py to run on port 5000 instead of 6000. 2025-04-21 18:43:58 +01:00
b6bf9f387d Update mindmap database: replace binary file with a new version to incorporate recent structural and data changes. 2025-04-21 18:26:41 +01:00
d9fe1f8efc Update mindmap database: replace existing binary file with a new version, reflecting recent changes in mindmap structure and data. 2025-04-20 20:28:51 +01:00
fd7bc59851 Add user authentication routes: implement login, registration, and logout functionality, along with user profile and admin routes. Enhance mindmap API with error handling and default node creation. 2025-04-20 19:58:27 +01:00
55f1f87509 Refactor app initialization: encapsulate Flask app setup and database initialization within a create_app function, improving modularity and error handling during startup. 2025-04-20 19:54:07 +01:00
03f8761312 Update Docker configuration to change exposed port from 5000 to 6000 in Dockerfile and docker-compose.yml, ensuring consistency across the application. 2025-04-20 19:48:49 +01:00
506748fda7 Implement error handling and default node creation for mindmap routes; initialize database on first request to ensure root node exists. 2025-04-20 19:43:21 +01:00
6d069f68cd Update requirements.txt to include new dependencies for enhanced functionality and remove outdated packages for better compatibility. 2025-04-20 19:32:32 +01:00
4310239a7a Enhance Dockerfile: add system dependencies for building Python packages, update requirements.txt to remove specific version constraints, and verify installations with pip list. 2025-04-20 19:31:13 +01:00
e9fe907af0 Update requirements.txt to include email_validator==2.1.1 for improved email validation functionality. 2025-04-20 19:29:19 +01:00
0c69d9aba3 Remove unnecessary 'force' option from docker-compose.yml for cleaner configuration. 2025-04-20 19:26:44 +01:00
6da85cdece Refactor Docker setup: update docker-compose.yml to use a specific website directory for volumes, enable automatic restarts, and modify Dockerfile to clean up and copy application files more efficiently. 2025-04-20 19:25:08 +01:00
a073b09115 Update Docker configuration: change Docker Compose version to 3.8, enhance web service setup with context and dockerfile specifications, add volume and environment variables for Flask development, and modify Dockerfile to use Python 3.11 and improve file copying and command execution. 2025-04-20 19:22:08 +01:00
f1f4870989 Update dependencies in requirements.txt to specific versions for Flask, Flask-Login, Flask-SQLAlchemy, Werkzeug, and SQLAlchemy 2025-04-20 19:16:34 +01:00
111 changed files with 29179 additions and 7210 deletions

6
.env
View File

@@ -2,7 +2,9 @@
# Kopiere diese Datei zu .env und passe die Werte an
# Flask
SECRET_KEY=dein-geheimer-schluessel-hier
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=your-secret-key-replace-in-production
# OpenAI API
OPENAI_API_KEY=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
@@ -10,4 +12,4 @@ OPENAI_API_KEY=sk-svcacct-yfmjXZXeB1tZqxp2VqSH1shwYo8QgSF8XNxEFS3IoWaIOvYvnCBxn5
# 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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
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
logs/app.log

File diff suppressed because it is too large Load Diff

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
# Dockerfile
FROM python:3.11-slim
# Arbeitsverzeichnis in Container
WORKDIR /app
# Systemabhängigkeiten installieren und Verzeichnisse anlegen
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/* && \
mkdir -p /app/database
# pip auf den neuesten Stand bringen und Requirements installieren
COPY requirements.txt ./
RUN pip install --upgrade pip && \
pip install --no-cache-dir -U -r requirements.txt
# Anwendungscode kopieren
COPY . .
# Berechtigungen für database-Ordner
RUN chmod -R 777 /app/database
# Exponiere Port 5000 für Flask
EXPOSE 5000
# Setze Umgebungsvariablen
ENV FLASK_APP=app.py
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Startkommando mit spezifischen Flags für Produktion
CMD ["python", "app.py"]

View File

@@ -1,172 +1,464 @@
# Systades Mindmap - Entwicklungs-Roadmap
# 🚀 SysTades Social Network - Entwicklungsroadmap
Diese Roadmap beschreibt die geplante Entwicklung der dynamischen, benutzerorientierten Mindmap-Funktionalität für das Systades-Projekt.
## 📋 Überblick
SysTades ist jetzt ein vollwertiges Social Network für Wissensaustausch, Mindmapping und Community-Building.
## Phase 1: Grundlegendes Datenmodell und Backend (Abgeschlossen ✅)
## Abgeschlossene Phasen
- [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 1: Basis Social Network ✅
- ✅ Erweiterte Benutzermodelle mit Social Features
- ✅ Posts, Kommentare, Likes, Follows System
- ✅ Benachrichtigungssystem
- ✅ Benutzerprofile mit Statistiken
- ✅ Erweiterte Navigation und UI
-**Verbessertes Logging-System mit visuellen Enhancements**
- ✅ Social Feed mit Filtering
- ✅ Mobile-responsive Design
## Phase 2: Dynamische Mindmap-Visualisierung (Abgeschlossen)
### Phase 2: Core Features
- ✅ Mindmap-Integration in Social Posts
- ✅ Gedanken-Sharing System
- ✅ Bookmark-System für Posts
- ✅ Analytics Dashboard für Benutzer
- ✅ Erweiterte Suche (Benutzer, Posts, Gedanken)
- ✅ Real-time Benachrichtigungen
- ✅ Post-Sharing und Engagement Metrics
- [x] Anpassung des Frontend-Codes zur Verwendung der DB-Daten anstelle des SVG
- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels
- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
- [x] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
- [x] Verbesserte Fehlerbehandlung in der Knotenvisualisierung
- [x] Robustere Verbindungserkennung zwischen Knoten
- [x] Implementierung von Glasmorphismus-Effekten für moderneres UI
### Phase 3: Erweiterte Social Features ✅
- ✅ Benutzerprofile mit Tabs (Posts, Gedanken, Mindmaps, Aktivität)
- ✅ Follow/Unfollow System mit UI
- ✅ Notification Center mit Filtering
- ✅ Post-Typen (Text, Gedanke, Frage, Erkenntnis)
- ✅ Sichtbarkeitseinstellungen (Öffentlich, Follower, Privat)
- ✅ Quick-Create Post Modal
## Phase 3: Visuelles Design und UX (Abgeschlossen ✅)
### Phase 3.5: Logging & Monitoring System ✅ (NEU)
-**Erweiterte SocialNetworkLogger Klasse mit visuellen Features**
-**Farbige Konsolen-Ausgabe mit ANSI-Codes**
-**Emoji-basierte Kategorisierung für bessere Übersicht**
-**Component-spezifisches Logging (AUTH, API, DB, ERROR, etc.)**
-**Performance-Monitoring mit Zeitstempel**
-**Strukturierte JSON-Logs für externe Analyse**
-**Decorator-basierte Instrumentierung**
-**Vollständige Integration in alle App-Komponenten**
-**Ersetzung aller print-Statements durch strukturierte Logs**
- [x] Implementierung des Dark Mode
- [x] Entwicklung eines modernen, minimalistischen UI
- [x] Animierter neuronaler Netzwerk-Hintergrund mit WebGL
- [x] Responsive Design für alle Geräte
- [x] Verbesserte Hover- und Selektionseffekte
- [x] Clustertopologie für neuronale Netzwerkdarstellung
- [x] Animierte Neuronenfeuer-Simulation mit Signalweiterleitung
## 🔄 Aktuelle Phase 4: UI/UX Verbesserungen (In Arbeit)
## Phase 4: Benutzerdefinierte Mindmaps (Aktuell 🔄)
### UI/UX Komponenten
- ✅ Moderne Navigation mit Icons und Badges
- ✅ Dark/Light Mode Toggle
- ✅ Responsive Mobile Navigation
- ✅ Glassmorphism Design Elements
- ✅ Gradient Themes und Farbsystem
- ✅ Toast Notification System
- ⏳ Chat/Messaging System
- ⏳ Story/Status Features
- ⏳ Advanced Image/Video Upload
- [x] UI für das Betrachten bestehender Mindmaps
- [ ] UI für das Erstellen und Bearbeiten 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
### Performance Optimierungen
- ⏳ Lazy Loading für Posts
- ⏳ Image Optimization
- ⏳ Caching System
- ⏳ API Rate Limiting
- ⏳ Database Indexing
## Phase 5: Notizen und Annotationen
## 📈 Kommende Phasen
- [x] Anzeige von Gedanken zu Mindmap-Knoten
- [ ] 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: Community Features
- 🔲 Gruppen/Communities System
- 🔲 Events und Kalenderfunktion
- 🔲 Live Discussions/Chats
- 🔲 Trending Topics/Hashtags
- 🔲 User Verification System
- 🔲 Moderation Tools
## Phase 6: Tagging und Quellenmanagement
### Phase 6: Advanced Features
- 🔲 AI-basierte Content Empfehlungen
- 🔲 Voice Notes und Audio Posts
- 🔲 Video Sharing und Streaming
- 🔲 Collaborative Mindmaps
- 🔲 Knowledge Graph Visualisierung
- 🔲 Advanced Analytics
- [ ] Tagging-System für Inhalte implementieren
- [ ] Verknüpfen von Quellen mit Mindmap-Knoten
- [ ] Upload-Funktionalität für Dateien und Medien
- [ ] Verwaltung von Zitaten und Referenzen
- [ ] Visuelles Feedback für Tags und Quellen in der Mindmap
### Phase 7: Monetarisierung & Skalierung
- 🔲 Premium Features
- 🔲 Creator Economy Tools
- 🔲 API für Drittanbieter
- 🔲 Mobile Apps (iOS/Android)
- 🔲 Enterprise Features
- 🔲 Advanced Security Features
## Phase 7: Integrationen und Erweiterungen
### Phase 8: Integration & Ecosystem
- 🔲 External Tool Integrations
- 🔲 Learning Management System
- 🔲 Knowledge Base Integration
- 🔲 Research Tools
- 🔲 Publication System
- 🔲 Academic Collaboration Tools
- [ ] 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
## 🏗️ Technische Architektur
## Phase 8: KI-Integration und Analyse
### Backend Stack ✅
- **Framework**: Flask mit SQLAlchemy
- **Datenbank**: SQLite (PostgreSQL für Produktion)
- **Authentifizierung**: Flask-Login
- **API**: RESTful JSON APIs
- **Logging**: **Erweiterte SocialNetworkLogger mit visuellen Features**
- **Farbige Konsolen-Ausgabe mit ANSI-Codes**
- **Emoji-basierte Kategorisierung (🔐 AUTH, 🌐 API, 💾 DB, etc.)**
- **Component-spezifisches Logging mit Performance-Monitoring**
- **JSON-strukturierte Logs für externe Analyse**
- **Decorator-basierte automatische Instrumentierung**
- **Performance**: Pagination, Caching
- [ ] 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
### Frontend Stack ✅
- **Styling**: TailwindCSS mit Custom Themes
- **JavaScript**: Vanilla JS mit ES6+ Features
- **Icons**: Font Awesome 6
- **Responsive**: Mobile-First Design
- **Interaktivität**: Alpine.js für reaktive Komponenten
## Phase 9: Optimierung und Skalierung
### Database Schema ✅
```sql
-- Core Tables
users (erweitert mit Social Features)
social_posts (Posts System)
social_comments (Kommentar System)
notifications (Benachrichtigungssystem)
user_settings (Benutzereinstellungen)
activities (Aktivitätsverfolgung)
- [ ] Performance-Optimierung für große Mindmaps
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
- [ ] Erweiterte Such- und Filterfunktionen
- [ ] Mobile Optimierung
- [ ] Offline-Funktionalität mit Synchronisierung
-- Relationship Tables
user_friendships (Freundschaftssystem)
user_follows (Follow System)
post_likes (Like System)
comment_likes (Comment Likes)
user_thought_bookmark (Bookmark System)
```
## Technische Schulden und Refactoring
## 📊 API Endpunkte
- [ ] 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
### Social Feed APIs ✅
- `GET /api/social/posts` - Feed Posts abrufen
- `POST /api/social/posts` - Neuen Post erstellen
- `POST /api/social/posts/{id}/like` - Post liken/unliken
- `POST /api/social/posts/{id}/share` - Post teilen
- `POST /api/social/posts/{id}/bookmark` - Post bookmarken
## KI-Integration
### User Management APIs ✅
- `GET /api/social/users/{id}` - Benutzerprofil abrufen
- `GET /api/social/users/search` - Benutzer suchen
- `POST /api/social/users/{id}/follow` - Benutzer folgen/entfolgen
### Aktuelle Implementation
- Integration von OpenAI mit dem gpt-4o-mini-Modell für den KI-Assistenten
- Datenbankzugriff für den KI-Assistenten, um direkt Informationen aus der Datenbank abzufragen
- Verbesserte Benutzeroberfläche für den KI-Assistenten mit kontextbezogenen Vorschlägen
### Notification APIs ✅
- `GET /api/social/notifications` - Benachrichtigungen abrufen
- `POST /api/social/notifications/{id}/read` - Als gelesen markieren
- `POST /api/social/notifications/mark-all-read` - Alle als gelesen
- `DELETE /api/social/notifications/{id}` - Benachrichtigung löschen
### Zukünftige Verbesserungen
- Implementierung von Vektorsuche für präzisere Datenbank-Abfragen durch die KI
- Erweiterung der KI-Funktionalität für tiefere Analyse von Zusammenhängen zwischen Gedanken
- KI-gestützte Vorschläge für neue Verbindungen zwischen Gedanken basierend auf Inhaltsanalyse
- Finetuning des KI-Modells auf die spezifischen Anforderungen der Anwendung
- Erweiterung auf multimodale Fähigkeiten (Bild- und Textanalyse)
### Analytics APIs ✅
- `GET /api/social/analytics/dashboard` - Benutzer-Analytics
- `GET /api/social/bookmarks` - Gebookmarkte Posts
## 🔒 Sicherheit & Datenschutz
### Implementierte Features ✅
- CSRF Protection
- SQL Injection Prevention
- Input Validation & Sanitization
- Session Management
- Password Hashing
- Privacy Controls (Post Visibility)
### Geplante Features
- 2FA Authentication
- Advanced Privacy Settings
- Data Export/Import
- GDPR Compliance Tools
- Content Moderation AI
## 📱 Mobile Support
### Aktuelle Features ✅
- Responsive Design
- Touch-Friendly Interface
- Mobile Navigation
- Optimized Loading
### Geplante Features
- PWA Support
- Offline Capabilities
- Push Notifications
- Native Mobile Apps
## 🎯 Leistungsziele
### Aktueller Status
- ✅ Grundlegende Performance
- ✅ Database Queries optimiert
- ✅ Frontend Responsiveness
- ✅ Strukturiertes Logging System
### Ziele für nächste Phase
- 🎯 < 200ms API Response Zeit
- 🎯 90+ Lighthouse Score
- 🎯 Skalierung auf 10k+ Benutzer
- 🎯 99.9% Uptime
## 🧪 Testing & Quality
### Implementiert
- ✅ Manuelle Testing
- ✅ Error Handling
-**Erweiterte Logging & Monitoring mit visuellen Features**
-**Farbige, kategorisierte Logs für bessere Debugging-Erfahrung**
-**Performance-Monitoring mit Zeitstempel**
-**Component-spezifische Fehlerbehandlung**
-**Strukturierte JSON-Logs für Analyse**
### Geplant
- 🔲 Automatisierte Unit Tests
- 🔲 Integration Tests
- 🔲 Performance Tests
- 🔲 Security Audits
- 🔲 Load Testing
- 🔲 **Log-basierte Alerting System**
- 🔲 **Automated Error Reporting**
## 📈 Metriken & Analytics
### User Engagement
- Posts pro Tag
- Kommentare und Likes
- Follow/Unfollow Raten
- Session Dauer
- Return User Rate
### System Performance
- API Response Zeiten
- Database Performance
- Error Rates
- User Activity Patterns
## 🛠️ Entwicklungsumgebung
### Setup Requirements ✅
```bash
# Virtual Environment
python3.11 -m venv venv
source venv/bin/activate
# Dependencies
pip install -r requirements.txt
# Database Migration
flask db upgrade
# Development Server
python3.11 app.py
```
### Development Tools ✅
- **IDE**: Cursor/VS Code
- **Version Control**: Git
- **Database**: SQLite (dev), PostgreSQL (prod)
- **Logging**: Colored Console + File Logging
- **Debug**: Flask Debug Mode
## 🌟 Innovation Features
### Einzigartige Aspekte
- 🧠 **Knowledge-First Design**: Fokus auf Wissensaustausch
- 🎨 **Mindmap Integration**: Visuelle Gedankenlandkarten
- 🔍 **Deep Search**: Semantic Search durch Inhalte
- 📊 **Learning Analytics**: Fortschritt und Erkenntnisse
- 🤝 **Collaborative Learning**: Gemeinsam Wissen erschaffen
### Zukünftige Innovationen
- 🤖 AI-Powered Knowledge Extraction
- 🎬 Interactive Learning Experiences
- 🌐 Cross-Platform Knowledge Sync
- 📚 Dynamic Knowledge Graphs
- 🧮 Algorithmic Learning Paths
---
## Implementierungsdetails
## 📝 Aktuelle Tasks
### Datenbankschema
### Hohe Priorität
1. ⏳ Chat/Messaging System implementieren
2. ⏳ Advanced Image Upload mit Preview
3. ⏳ Performance Optimierungen
4. ⏳ Mobile App Prototyp
Das Datenbankschema umfasst folgende Hauptentitäten:
### Mittlere Priorität
1. 🔲 Gruppen/Communities Feature
2. 🔲 Advanced Analytics Dashboard
3. 🔲 Content Moderation Tools
4. 🔲 API Rate Limiting
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
### Niedrige Priorität
1. 🔲 Email Benachrichtigungen
2. 🔲 Export/Import Features
3. 🔲 Advanced Search Filters
4. 🔲 Theming System
### Frontend-Technologien
---
- D3.js für die Visualisierung der Mindmap
- WebGL für den neuronalen Netzwerk-Hintergrund
- AJAX für dynamisches Laden von Daten
- Interaktive Bedienelemente mit JavaScript
- Responsive Design mit Tailwind CSS
**Letzte Aktualisierung**: {{ current_date }}
**Version**: 2.0.0 - Social Network Release
**Status**: ✅ Fully Functional Social Platform
### Backend-APIs
# 🗺️ SysTades Roadmap
Die implementierten API-Endpunkte umfassen:
## ✅ Abgeschlossen (v1.0 - v1.3)
- `/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
- `/api/get_dark_mode` - Abrufen der Dark Mode Einstellung
### 🎯 Grundfunktionen
- [x] **Benutzerauthentifizierung** - Registrierung, Login, Logout
- [x] **Interaktive Mindmap** - Cytoscape.js-basierte Visualisierung
- [x] **Gedankenverwaltung** - CRUD-Operationen für Thoughts
- [x] **Kategoriesystem** - Hierarchische Wissensorganisation
- [x] **Responsive Design** - Mobile-first Ansatz
- [x] **Dark/Light Mode** - Benutzerfreundliche Themes
## Neuronaler Netzwerk-Hintergrund
### 🤖 KI-Integration
- [x] **ChatGPT-Assistent** - Integrierter AI-Chat
- [x] **Intelligente Suche** - KI-gestützte Inhaltssuche
- [x] **Automatische Kategorisierung** - AI-basierte Thought-Klassifizierung
Der neue WebGL-basierte Hintergrund bietet:
### 🎨 UI/UX Verbesserungen
- [x] **Moderne Navigation** - Glassmorphism-Design
- [x] **Animationen** - Smooth Transitions und Hover-Effekte
- [x] **Accessibility** - ARIA-Labels und Keyboard-Navigation
- [x] **Performance-Optimierung** - Lazy Loading und Caching
- WebGL-basierte Rendering-Engine für optimale Performance
- Dynamische Knoten und Verbindungen mit realistischem Verhalten
- Clustering von neuronalen Knoten für natürlicheres Erscheinungsbild
- Simulation von neuronaler Aktivität und Signalweiterleitung
- Anpassbare visuelle Parameter (Helligkeit, Dichte, Geschwindigkeit)
- Vollständig responsives Design für alle Bildschirmgrößen
## 🚀 Neu implementiert (v1.4 - Social Network Update)
## Aktuelle Verbesserungen
- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024)
- Content Security Policy (CSP) für Tailwind CSS CDN und WebGL konfiguriert
- Behebung kritischer Fehler in der Mindmap-Knotenvisualisierung (15.06.2024)
- Verbesserte Verbindungserkennung zwischen Knoten implementiert
- Robuste Fehlerbehandlung für verschiedene API-Datenformate
### 📱 Social Network Features
- [x] **Social Feed** - Instagram/Twitter-ähnlicher Feed
- [x] **Post-System** - Erstellen, Liken, Kommentieren von Posts
- [x] **Follow-System** - Benutzer folgen und entfolgen
- [x] **Discover-Seite** - Trending Posts und empfohlene Benutzer
- [x] **Benutzerprofile** - Erweiterte Profile mit Posts, Mindmaps, Gedanken
- [x] **Benachrichtigungssystem** - Likes, Kommentare, Follows
- [x] **Community-Statistiken** - Aktive Benutzer, Posts, Mindmaps
## Zukünftige Aufgaben (Q3 2024)
- Implementierung des Tagging-Systems für Gedanken
- Quellenmanagement für Mindmap-Knoten
- Erweiterte Benutzerprofilfunktionen
- Verbesserung der mobilen Benutzererfahrung
- Integration von Exportfunktionen für Mindmaps
### 🧠 Erweiterte Mindmap-Features
- [x] **Kollaborative Bearbeitung** - Vorbereitung für Echtzeit-Kollaboration
- [x] **Mindmap-Export** - JSON-Export mit geplanten weiteren Formaten
- [x] **Mindmap-Sharing** - Teilen von Mindmaps in sozialen Netzwerken
- [x] **Erweiterte Toolbar** - Neue Bearbeitungsoptionen
- [x] **Vollbild-Modus** - Immersive Mindmap-Bearbeitung
- [x] **Schnelle Knoten-/Gedanken-Erstellung** - Direkt aus der Mindmap
*Zuletzt aktualisiert: 15.06.2024*
### 🔗 Integration & Vernetzung
- [x] **Gedanken in Posts teilen** - Wissenschaftliche Inhalte im Feed
- [x] **Mindmap-Knoten teilen** - Wissensbausteine verbreiten
- [x] **Cross-Platform Navigation** - Nahtlose Übergänge zwischen Features
- [x] **Unified Search** - Suche über alle Inhaltstypen
## [Entfernt] CORS-Unterstützung (flask-cors)
- Die flask-cors-Bibliothek und alle zugehörigen Initialisierungen wurden entfernt.
- CORS wird nicht mehr unterstützt oder benötigt.
## 🔄 In Entwicklung (v1.5)
### 🔄 Echtzeit-Features
- [ ] **Live-Kollaboration** - Mehrere Benutzer bearbeiten gleichzeitig Mindmaps
- [ ] **WebSocket-Integration** - Echtzeit-Updates für Feed und Benachrichtigungen
- [ ] **Live-Cursor** - Sehen wo andere Benutzer arbeiten
- [ ] **Änderungshistorie** - Versionskontrolle für Mindmaps
### 💬 Erweiterte Kommunikation
- [ ] **Direktnachrichten** - Private Nachrichten zwischen Benutzern
- [ ] **Gruppen-Chats** - Themenbasierte Diskussionsgruppen
- [ ] **Video-Calls** - Integrierte Videokonferenzen für Kollaboration
- [ ] **Screen-Sharing** - Bildschirm teilen während Kollaboration
## 📋 Geplant (v1.6 - v2.0)
### 📊 Analytics & Insights
- [ ] **Lernfortschritt-Tracking** - Persönliche Wissensstatistiken
- [ ] **Mindmap-Analytics** - Nutzungsstatistiken und Hotspots
- [ ] **Community-Insights** - Trending-Themen und beliebte Inhalte
- [ ] **Empfehlungsalgorithmus** - Personalisierte Inhaltsvorschläge
### 🎓 Bildungsfeatures
- [ ] **Kurssystem** - Strukturierte Lernpfade
- [ ] **Quizzes & Tests** - Wissensüberprüfung
- [ ] **Zertifikate** - Digitale Abschlüsse
- [ ] **Mentoring-System** - Experten-Schüler-Verbindungen
### 🔧 Erweiterte Tools
- [ ] **PDF-Import** - Automatische Mindmap-Generierung aus Dokumenten
- [ ] **LaTeX-Support** - Mathematische Formeln in Gedanken
- [ ] **Multimedia-Integration** - Videos, Audio, Bilder in Mindmaps
- [ ] **API für Drittanbieter** - Integration mit anderen Tools
### 🌐 Skalierung & Performance
- [ ] **Microservices-Architektur** - Bessere Skalierbarkeit
- [ ] **CDN-Integration** - Globale Content-Delivery
- [ ] **Caching-Optimierung** - Redis für bessere Performance
- [ ] **Load Balancing** - Hochverfügbarkeit
## 🔮 Vision (v2.0+)
### 🤖 Erweiterte KI
- [ ] **Personalisierte KI-Tutoren** - Individuelle Lernbegleitung
- [ ] **Automatische Mindmap-Generierung** - KI erstellt Mindmaps aus Text
- [ ] **Intelligente Verbindungen** - KI schlägt Gedankenverknüpfungen vor
- [ ] **Adaptive Lernpfade** - KI passt Inhalte an Lernstil an
### 🌍 Globale Community
- [ ] **Mehrsprachigkeit** - Internationale Benutzergemeinschaft
- [ ] **Kultureller Austausch** - Globale Wissensnetzwerke
- [ ] **Übersetzungsfeatures** - Automatische Inhaltsübersetzung
- [ ] **Regionale Communities** - Lokale Wissensgruppen
### 🔬 Forschungstools
- [ ] **Literaturverwaltung** - Integration mit wissenschaftlichen Datenbanken
- [ ] **Zitiersystem** - Automatische Quellenangaben
- [ ] **Peer-Review-System** - Wissenschaftliche Qualitätskontrolle
- [ ] **Publikationstools** - Direkte Veröffentlichung von Forschungsergebnissen
---
## 📈 Metriken & Ziele
### Technische Ziele
- **Performance**: < 2s Ladezeit für alle Seiten
- **Verfügbarkeit**: 99.9% Uptime
- **Skalierbarkeit**: 10.000+ gleichzeitige Benutzer
- **Sicherheit**: Zero-Trust-Architektur
### Community-Ziele
- **Benutzer**: 1.000+ aktive Benutzer bis Ende 2024
- **Inhalte**: 10.000+ Gedanken und 1.000+ Mindmaps
- **Engagement**: 70%+ monatliche Aktivitätsrate
- **Zufriedenheit**: 4.5+ Sterne Bewertung
---
## 🤝 Beitragen
Interessiert an der Mitarbeit? Hier sind die Bereiche, in denen wir Unterstützung suchen:
### 👨‍💻 Entwicklung
- **Frontend**: React/Vue.js Komponenten
- **Backend**: Python/Flask API-Entwicklung
- **Mobile**: React Native App
- **DevOps**: Docker, Kubernetes, CI/CD
### 🎨 Design
- **UI/UX**: Benutzeroberflächen-Design
- **Grafik**: Icons, Illustrationen, Branding
- **Animation**: Micro-Interactions und Transitions
- **Accessibility**: Barrierefreie Gestaltung
### 📝 Content
- **Dokumentation**: Technische und Benutzer-Dokumentation
- **Tutorials**: Video- und Text-Anleitungen
- **Übersetzungen**: Mehrsprachige Inhalte
- **Community**: Moderation und Support
---
*Letzte Aktualisierung: Januar 2024*
*Version: 1.4.0 - Social Network Update*

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2785
app.py

File diff suppressed because it is too large Load Diff

2526
app.py.bak Normal file

File diff suppressed because it is too large Load Diff

BIN
backup/archiv_0.1.zip Normal file

Binary file not shown.

5
cookies.txt Normal file
View File

@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_127.0.0.1 FALSE / FALSE 0 session .eJwlzjEOwjAMQNG7ZGaIYztJe5nKjm2BACG1dELcnUiMf3n6n7TF7sc1re_99EvabpbWZI7cikB4dsoylLrmcKXSormH-OhKoQRSAy0v3kEzDqJlSNFCg8NIW25sfYChAgryFIWxdqyskqFWtNIdiF3awiZRaq9TzGmOnIfv_xuYabLft-fLPK0hj8O_P-1dNpA.aDdoog.bmKi2y6o3HQgIk4gwDvhirnxuoM

Binary file not shown.

17
docker-compose.yml Normal file
View File

@@ -0,0 +1,17 @@
version: '3.9'
services:
web:
build: .
image: systades_app:latest
container_name: systades_app
restart: always
env_file:
- .env
ports:
- "5000:5000"
volumes:
- ./database:/app/database
volumes:
db_data:

View File

@@ -2,12 +2,14 @@
# Kopiere diese Datei zu .env und passe die Werte an
# Flask
SECRET_KEY=dein-geheimer-schluessel-hier
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=mein-sicherer-schluessel-fuer-entwicklung
# OpenAI API
OPENAI_API_KEY=sk-dein-openai-api-schluessel-hier
OPENAI_API_KEY=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
# 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
SQLALCHEMY_DATABASE_URI=sqlite:///database/systades.db

View File

@@ -1,19 +1,29 @@
#!/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
import sqlite3
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
# Pfad zur Datenbank
basedir = os.path.abspath(os.path.dirname(__file__))
db_path = os.path.join(basedir, 'database', 'systades.db')
# Stelle sicher, dass das Verzeichnis existiert
db_dir = os.path.dirname(db_path)
os.makedirs(db_dir, exist_ok=True)
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///systades.db'
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Importiere die Modelle nach der App-Initialisierung
from models import db, User, Thought, Comment, MindMapNode, ThoughtRelation, ThoughtRating, RelationType
from models import Category, UserMindmap, UserMindmapNode, MindmapNote
db.init_app(app)
def init_db():
@@ -69,45 +79,111 @@ def create_default_users():
def create_default_categories():
"""Erstellt die Standardkategorien für die Mindmap"""
categories = [
# Hauptkategorien
main_categories = [
{
'name': 'Konzept',
'description': 'Abstrakte Ideen und theoretische Konzepte',
'color_code': '#6366f1',
'icon': 'lightbulb'
"name": "Philosophie",
"description": "Philosophisches Denken und Konzepte",
"color_code": "#9F7AEA",
"icon": "fa-brain"
},
{
'name': 'Technologie',
'description': 'Hardware, Software, Tools und Plattformen',
'color_code': '#10b981',
'icon': 'cpu'
"name": "Wissenschaft",
"description": "Wissenschaftliche Disziplinen und Erkenntnisse",
"color_code": "#60A5FA",
"icon": "fa-flask"
},
{
'name': 'Prozess',
'description': 'Workflows, Methodologien und Vorgehensweisen',
'color_code': '#f59e0b',
'icon': 'git-branch'
"name": "Technologie",
"description": "Technologische Entwicklungen und Anwendungen",
"color_code": "#10B981",
"icon": "fa-microchip"
},
{
'name': 'Person',
'description': 'Personen, Teams und Organisationen',
'color_code': '#ec4899',
'icon': 'user'
"name": "Künste",
"description": "Künstlerische Ausdrucksformen und Werke",
"color_code": "#F59E0B",
"icon": "fa-palette"
},
{
'name': 'Dokument',
'description': 'Dokumentationen, Referenzen und Ressourcen',
'color_code': '#3b82f6',
'icon': 'file-text'
"name": "Psychologie",
"description": "Mentale Prozesse und Verhaltensweisen",
"color_code": "#EF4444",
"icon": "fa-brain"
}
]
for cat_data in categories:
# Hauptkategorien erstellen
category_map = {}
for cat_data in main_categories:
category = Category(**cat_data)
db.session.add(category)
db.session.flush() # ID generieren
category_map[cat_data["name"]] = category
# Unterkategorien für Philosophie
philosophy_subcategories = [
{"name": "Ethik", "description": "Moralische Grundsätze", "icon": "fa-balance-scale", "color_code": "#8B5CF6"},
{"name": "Logik", "description": "Gesetze des Denkens", "icon": "fa-project-diagram", "color_code": "#8B5CF6"},
{"name": "Erkenntnistheorie", "description": "Natur des Wissens", "icon": "fa-lightbulb", "color_code": "#8B5CF6"}
]
# Unterkategorien für Wissenschaft
science_subcategories = [
{"name": "Physik", "description": "Studie der Materie und Energie", "icon": "fa-atom", "color_code": "#3B82F6"},
{"name": "Biologie", "description": "Studie des Lebens", "icon": "fa-dna", "color_code": "#3B82F6"},
{"name": "Mathematik", "description": "Studie der Zahlen und Strukturen", "icon": "fa-square-root-alt", "color_code": "#3B82F6"}
]
# Unterkategorien für Technologie
tech_subcategories = [
{"name": "Software", "description": "Computerprogramme und Anwendungen", "icon": "fa-code", "color_code": "#059669"},
{"name": "Hardware", "description": "Physische Komponenten der Technik", "icon": "fa-microchip", "color_code": "#059669"},
{"name": "Internet", "description": "Globales Netzwerk und Web", "icon": "fa-globe", "color_code": "#059669"}
]
# Unterkategorien für Künste
arts_subcategories = [
{"name": "Musik", "description": "Klangkunst", "icon": "fa-music", "color_code": "#D97706"},
{"name": "Literatur", "description": "Geschriebene Kunst", "icon": "fa-book", "color_code": "#D97706"},
{"name": "Bildende Kunst", "description": "Visuelle Kunst", "icon": "fa-paint-brush", "color_code": "#D97706"}
]
# Unterkategorien für Psychologie
psychology_subcategories = [
{"name": "Kognition", "description": "Gedächtnisprozesse und Denken", "icon": "fa-brain", "color_code": "#DC2626"},
{"name": "Emotionen", "description": "Gefühle und emotionale Prozesse", "icon": "fa-heart", "color_code": "#DC2626"},
{"name": "Verhalten", "description": "Beobachtbares Verhalten und Reaktionen", "icon": "fa-user", "color_code": "#DC2626"}
]
# Alle Unterkategorien zu ihren Hauptkategorien hinzufügen
for subcat_data in philosophy_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Philosophie"].id
db.session.add(subcat)
for subcat_data in science_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Wissenschaft"].id
db.session.add(subcat)
for subcat_data in tech_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Technologie"].id
db.session.add(subcat)
for subcat_data in arts_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Künste"].id
db.session.add(subcat)
for subcat_data in psychology_subcategories:
subcat = Category(**subcat_data)
subcat.parent_id = category_map["Psychologie"].id
db.session.add(subcat)
db.session.commit()
print(f"{len(categories)} Kategorien wurden erstellt.")
print(f"{len(main_categories)} Hauptkategorien und {len(philosophy_subcategories + science_subcategories + tech_subcategories + arts_subcategories + psychology_subcategories)} Unterkategorien wurden erstellt.")
def create_sample_mindmap():
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""

0
instance/logs/errors.log Normal file
View File

0
instance/logs/social.log Normal file
View File

0
logs/api.log Normal file
View File

2419
logs/app.log Normal file

File diff suppressed because it is too large Load Diff

424
logs/errors.log Normal file
View File

@@ -0,0 +1,424 @@
2025-05-28 21:29:08 | ERROR | SysTades | ERROR | Fehler 500: 405 Method Not Allowed: The method is not allowed for the requested URL.
Endpoint: /api/thoughts, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 619, in match
raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
werkzeug.exceptions.MethodNotAllowed: 405 Method Not Allowed: The method is not allowed for the requested URL.
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler in social_feed nach 2.83ms - Exception: AttributeError: followed_id
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
return self._index[key][1]
~~~~~~~~~~~^^^^^
KeyError: 'followed_id'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2774, in social_feed
followed_posts = current_user.get_feed_posts(limit=100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
followed_users, SocialPost.user_id == followed_users.c.followed_id
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
raise AttributeError(key) from err
AttributeError: followed_id
2025-05-28 21:43:40 | ERROR | SysTades | ERROR | Fehler 500: followed_id
Endpoint: /feed, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1633, in __getattr__
return self._index[key][1]
~~~~~~~~~~~^^^^^
KeyError: 'followed_id'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
return current_app.ensure_sync(func)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2774, in social_feed
followed_posts = current_user.get_feed_posts(limit=100)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/models.py", line 193, in get_feed_posts
followed_users, SocialPost.user_id == followed_users.c.followed_id
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/base.py", line 1635, in __getattr__
raise AttributeError(key) from err
AttributeError: followed_id
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler in discover nach 16.89ms - Exception: AttributeError: 'AppenderQuery' object has no attribute 'contains'
Traceback (most recent call last):
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2800, in discover
~current_user.following.contains(User.id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'AppenderQuery' object has no attribute 'contains'
2025-05-28 21:43:59 | ERROR | SysTades | ERROR | Fehler 500: 'AppenderQuery' object has no attribute 'contains'
Endpoint: /discover, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
return current_app.ensure_sync(func)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2800, in discover
~current_user.following.contains(User.id)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'AppenderQuery' object has no attribute 'contains'
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler in social_feed nach 54.92ms - Exception: OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: near "UNION": syntax error
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2782, in social_feed
posts = all_posts.paginate(
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
return QueryPagination(
^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
items = self._query_items()
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
out = query.limit(self.per_page).offset(self._query_offset).all()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
return self._iter().all() # type: ignore
^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
return self._execute_internal(
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
result: Result[Any] = compile_state_cls.orm_execute_statement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
result = conn.execute(
^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
return meth(
^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
return connection._execute_clauseelement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
ret = self._execute_context(
^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
return self._exec_single_context(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
self._handle_dbapi_exception(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-05-28 21:46:15 | ERROR | SysTades | ERROR | Fehler 500: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
Endpoint: /feed, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: near "UNION": syntax error
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_login/utils.py", line 290, in decorated_view
return current_app.ensure_sync(func)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/utils/logger.py", line 586, in wrapper
result = func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 2782, in social_feed
posts = all_posts.paginate(
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/query.py", line 98, in paginate
return QueryPagination(
^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 72, in __init__
items = self._query_items()
^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py", line 358, in _query_items
out = query.limit(self.per_page).offset(self._query_offset).all()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2693, in all
return self._iter().all() # type: ignore
^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/query.py", line 2847, in _iter
result: Union[ScalarResult[_T], Result[_T]] = self.session.execute(
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2308, in execute
return self._execute_internal(
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2190, in _execute_internal
result: Result[Any] = compile_state_cls.orm_execute_statement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/orm/context.py", line 293, in orm_execute_statement
result = conn.execute(
^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
return meth(
^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 516, in _execute_on_connection
return connection._execute_clauseelement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1639, in _execute_clauseelement
ret = self._execute_context(
^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
return self._exec_single_context(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
self._handle_dbapi_exception(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2343, in _handle_dbapi_exception
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/home/core/.local/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 922, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) near "UNION": syntax error
[SQL: SELECT anon_1.social_post_id AS anon_1_social_post_id, anon_1.social_post_content AS anon_1_social_post_content, anon_1.social_post_image_url AS anon_1_social_post_image_url, anon_1.social_post_video_url AS anon_1_social_post_video_url, anon_1.social_post_link_url AS anon_1_social_post_link_url, anon_1.social_post_link_title AS anon_1_social_post_link_title, anon_1.social_post_link_description AS anon_1_social_post_link_description, anon_1.social_post_post_type AS anon_1_social_post_post_type, anon_1.social_post_visibility AS anon_1_social_post_visibility, anon_1.social_post_is_pinned AS anon_1_social_post_is_pinned, anon_1.social_post_like_count AS anon_1_social_post_like_count, anon_1.social_post_comment_count AS anon_1_social_post_comment_count, anon_1.social_post_share_count AS anon_1_social_post_share_count, anon_1.social_post_view_count AS anon_1_social_post_view_count, anon_1.social_post_created_at AS anon_1_social_post_created_at, anon_1.social_post_updated_at AS anon_1_social_post_updated_at, anon_1.social_post_user_id AS anon_1_social_post_user_id, anon_1.social_post_shared_thought_id AS anon_1_social_post_shared_thought_id, anon_1.social_post_shared_node_id AS anon_1_social_post_shared_node_id
FROM ((SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id IN (?) ORDER BY social_post.created_at DESC
LIMIT ? OFFSET ?) UNION SELECT social_post.id AS social_post_id, social_post.content AS social_post_content, social_post.image_url AS social_post_image_url, social_post.video_url AS social_post_video_url, social_post.link_url AS social_post_link_url, social_post.link_title AS social_post_link_title, social_post.link_description AS social_post_link_description, social_post.post_type AS social_post_post_type, social_post.visibility AS social_post_visibility, social_post.is_pinned AS social_post_is_pinned, social_post.like_count AS social_post_like_count, social_post.comment_count AS social_post_comment_count, social_post.share_count AS social_post_share_count, social_post.view_count AS social_post_view_count, social_post.created_at AS social_post_created_at, social_post.updated_at AS social_post_updated_at, social_post.user_id AS social_post_user_id, social_post.shared_thought_id AS social_post_shared_thought_id, social_post.shared_node_id AS social_post_shared_node_id
FROM social_post
WHERE social_post.user_id = ?) AS anon_1 ORDER BY anon_1.social_post_created_at DESC
LIMIT ? OFFSET ?]
[parameters: (1, 100, 0, 1, 10, 0)]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
2025-05-28 21:48:48 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
Nicht angemeldet
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
raise NotFound() from None
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:48:54 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /static/fonts/inter-regular.woff2, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 257, in <lambda>
view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 305, in send_static_file
return send_from_directory(
^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/helpers.py", line 554, in send_from_directory
return werkzeug.utils.send_from_directory( # type: ignore[return-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/utils.py", line 574, in send_from_directory
raise NotFound()
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:49:17 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /sw.js, Method: GET, IP: 127.0.0.1
Nicht angemeldet
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
raise NotFound() from None
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:55:55 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:56:25 | ERROR | SysTades | ERROR | Fehler 404: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Endpoint: /auth/login, Method: GET, IP: 127.0.0.1
Nicht angemeldet
Traceback (most recent call last):
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 841, in dispatch_request
self.raise_routing_exception(req)
File "/home/core/.local/lib/python3.11/site-packages/flask/app.py", line 450, in raise_routing_exception
raise request.routing_exception # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/flask/ctx.py", line 353, in match_request
result = self.url_adapter.match(return_rule=True) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/core/.local/lib/python3.11/site-packages/werkzeug/routing/map.py", line 624, in match
raise NotFound() from None
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
2025-05-28 21:56:41 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:57:25 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
not_following_subquery = db.session.query(user_follows.c.followed_id).filter(
^^^^^^^
NameError: name 'follows' is not defined
2025-05-28 21:58:02 | ERROR | SysTades | ERROR | Fehler 500: name 'follows' is not defined
Endpoint: /api/discover/users, Method: GET, IP: 127.0.0.1
User: 1 (admin)
Traceback (most recent call last):
File "/home/core/dev/website/app.py", line 424, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/home/core/dev/website/app.py", line 3141, in discover_users
users = User.query.filter(
NameError: name 'follows' is not defined

Binary file not shown.

View File

@@ -0,0 +1,38 @@
"""add mindmap shares table
Revision ID: add_mindmap_shares
Revises: add_missing_user_fields
Create Date: 2025-05-10 23:20:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = 'add_mindmap_shares'
down_revision = 'add_missing_user_fields'
branch_labels = None
depends_on = None
def upgrade():
# Erstelle PermissionType Enum
op.create_table('mindmap_share',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('mindmap_id', sa.Integer(), nullable=False),
sa.Column('shared_by_id', sa.Integer(), nullable=False),
sa.Column('shared_with_id', sa.Integer(), nullable=False),
sa.Column('permission_type', sa.String(20), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('last_accessed', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['mindmap_id'], ['user_mindmap.id'], ),
sa.ForeignKeyConstraint(['shared_by_id'], ['user.id'], ),
sa.ForeignKeyConstraint(['shared_with_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share')
)
def downgrade():
op.drop_table('mindmap_share')

View File

@@ -0,0 +1,40 @@
"""Add missing user fields
Revision ID: 5a23f8c6db37
Revises: d4406f5b12f7
Create Date: 2025-05-02 10:45:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5a23f8c6db37'
down_revision = 'd4406f5b12f7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('bio', sa.Text(), nullable=True))
batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True))
batch_op.add_column(sa.Column('website', sa.String(length=200), nullable=True))
batch_op.add_column(sa.Column('avatar', sa.String(length=200), nullable=True))
batch_op.add_column(sa.Column('last_login', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('last_login')
batch_op.drop_column('avatar')
batch_op.drop_column('website')
batch_op.drop_column('location')
batch_op.drop_column('bio')
# ### end Alembic commands ###

391
models.py
View File

@@ -45,6 +45,35 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
# Beziehungstabelle für Benutzer-Freundschaften
user_friendships = db.Table('user_friendships',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('friend_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow),
db.Column('status', db.String(20), default='pending') # pending, accepted, blocked
)
# Beziehungstabelle für Benutzer-Follows
user_follows = db.Table('user_follows',
db.Column('follower_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('followed_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
# Beziehungstabelle für Post-Likes
post_likes = db.Table('post_likes',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('post_id', db.Integer, db.ForeignKey('social_post.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
# Beziehungstabelle für Comment-Likes
comment_likes = db.Table('comment_likes',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('comment_id', db.Integer, db.ForeignKey('social_comment.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow)
)
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
@@ -53,11 +82,64 @@ class User(db.Model, UserMixin):
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
bio = db.Column(db.Text, nullable=True) # Profil-Bio
location = db.Column(db.String(100), nullable=True) # Standort
website = db.Column(db.String(200), nullable=True) # Website
avatar = db.Column(db.String(200), nullable=True) # Profilbild-URL
last_login = db.Column(db.DateTime, nullable=True) # Letzter Login
# Relationships
# Social Network Felder
display_name = db.Column(db.String(100), nullable=True) # Anzeigename
birth_date = db.Column(db.Date, nullable=True) # Geburtsdatum
gender = db.Column(db.String(20), nullable=True) # Geschlecht
phone = db.Column(db.String(20), nullable=True) # Telefonnummer
is_verified = db.Column(db.Boolean, default=False) # Verifizierter Account
is_private = db.Column(db.Boolean, default=False) # Privater Account
follower_count = db.Column(db.Integer, default=0) # Follower-Anzahl
following_count = db.Column(db.Integer, default=0) # Following-Anzahl
post_count = db.Column(db.Integer, default=0) # Post-Anzahl
online_status = db.Column(db.String(20), default='offline') # online, offline, away
last_seen = db.Column(db.DateTime, nullable=True) # Zuletzt gesehen
# Beziehungen
threads = db.relationship('Thread', backref='creator', lazy=True)
messages = db.relationship('Message', backref='author', lazy=True)
projects = db.relationship('Project', backref='owner', lazy=True)
mindmaps = db.relationship('UserMindmap', backref='user', lazy=True)
thoughts = db.relationship('Thought', backref='author', lazy=True)
bookmarked_thoughts = db.relationship('Thought', secondary=user_thought_bookmark,
lazy='dynamic', backref=db.backref('bookmarked_by', lazy='dynamic'))
# Social Network Beziehungen
posts = db.relationship('SocialPost', backref='author', lazy=True, cascade="all, delete-orphan")
comments = db.relationship('SocialComment', backref='author', lazy=True, cascade="all, delete-orphan")
notifications = db.relationship('Notification', foreign_keys='Notification.user_id', backref='user', lazy=True, cascade="all, delete-orphan")
# Freundschaften (bidirektional)
friends = db.relationship(
'User',
secondary=user_friendships,
primaryjoin=id == user_friendships.c.user_id,
secondaryjoin=id == user_friendships.c.friend_id,
backref='friend_of',
lazy='dynamic'
)
# Following/Followers
following = db.relationship(
'User',
secondary=user_follows,
primaryjoin=id == user_follows.c.follower_id,
secondaryjoin=id == user_follows.c.followed_id,
backref=db.backref('followers', lazy='dynamic'),
lazy='dynamic'
)
# Liked Posts und Comments
liked_posts = db.relationship('SocialPost', secondary=post_likes,
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
liked_comments = db.relationship('SocialComment', secondary=comment_likes,
backref=db.backref('liked_by', lazy='dynamic'), lazy='dynamic')
def __repr__(self):
return f'<User {self.username}>'
@@ -67,6 +149,53 @@ class User(db.Model, UserMixin):
def check_password(self, password):
return check_password_hash(self.password, password)
@property
def is_admin(self):
return self.role == 'admin'
@is_admin.setter
def is_admin(self, value):
self.role = 'admin' if value else 'user'
# Social Network Methoden
def follow(self, user):
"""Folgt einem anderen Benutzer"""
if not self.is_following(user):
self.following.append(user)
user.follower_count += 1
user.following_count += 1
# Notification erstellen
notification = Notification(
user_id=user.id,
type='follow',
message=f'{self.username} folgt dir jetzt',
related_user_id=self.id
)
db.session.add(notification)
def unfollow(self, user):
"""Entfolgt einem Benutzer"""
if self.is_following(user):
self.following.remove(user)
user.follower_count -= 1
user.following_count -= 1
def is_following(self, user):
"""Prüft ob der Benutzer einem anderen folgt"""
return self.following.filter(user_follows.c.followed_id == user.id).count() > 0
def get_feed_posts(self, limit=20):
"""Holt Posts für den Feed (von gefolgten Benutzern)"""
# Hole alle User-IDs von Benutzern, denen ich folge + meine eigene
followed_user_ids = [user.id for user in self.following]
all_user_ids = followed_user_ids + [self.id]
# Hole Posts von diesen Benutzern
return SocialPost.query.filter(
SocialPost.user_id.in_(all_user_ids)
).order_by(SocialPost.created_at.desc()).limit(limit)
class Category(db.Model):
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
@@ -305,4 +434,262 @@ class Document(db.Model):
file_size = db.Column(db.Integer, nullable=True)
def __repr__(self):
return f'<Document {self.title}>'
return f'<Document {self.title}>'
# Forum-Kategorie-Modell - entspricht den Hauptknotenpunkten der Mindmap
class ForumCategory(db.Model):
id = db.Column(db.Integer, primary_key=True)
node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=False)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
# Beziehungen
node = db.relationship('MindMapNode', backref='forum_category')
posts = db.relationship('ForumPost', backref='category', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<ForumCategory {self.title}>'
# Forum-Beitrag-Modell
class ForumPost(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('forum_category.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'), nullable=True)
is_pinned = db.Column(db.Boolean, default=False)
is_locked = db.Column(db.Boolean, default=False)
view_count = db.Column(db.Integer, default=0)
# Beziehungen
author = db.relationship('User', backref='forum_posts')
replies = db.relationship('ForumPost', backref=db.backref('parent', remote_side=[id]), lazy=True)
def __repr__(self):
return f'<ForumPost {self.title}>'
# Berechtigungstypen für Mindmap-Freigaben
class PermissionType(Enum):
READ = "Nur-Lesen"
EDIT = "Bearbeiten"
ADMIN = "Administrator"
# Freigabemodell für Mindmaps
class MindmapShare(db.Model):
"""Speichert Informationen über freigegebene Mindmaps und Berechtigungen"""
id = db.Column(db.Integer, primary_key=True)
mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=False)
shared_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
shared_with_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
permission_type = db.Column(db.Enum(PermissionType), nullable=False, default=PermissionType.READ)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_accessed = db.Column(db.DateTime, nullable=True)
# Beziehungen
mindmap = db.relationship('UserMindmap', backref=db.backref('shares', lazy='dynamic'))
shared_by = db.relationship('User', foreign_keys=[shared_by_id], backref=db.backref('shared_mindmaps', lazy='dynamic'))
shared_with = db.relationship('User', foreign_keys=[shared_with_id], backref=db.backref('accessible_mindmaps', lazy='dynamic'))
__table_args__ = (
db.UniqueConstraint('mindmap_id', 'shared_with_id', name='unique_mindmap_share'),
)
def __repr__(self):
return f'<MindmapShare: {self.mindmap_id} - {self.shared_with_id} - {self.permission_type.name}>'
class SocialPost(db.Model):
"""Posts im Social Network"""
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
image_url = db.Column(db.String(500), nullable=True) # Bild-URL
video_url = db.Column(db.String(500), nullable=True) # Video-URL
link_url = db.Column(db.String(500), nullable=True) # Link-URL
link_title = db.Column(db.String(200), nullable=True) # Link-Titel
link_description = db.Column(db.Text, nullable=True) # Link-Beschreibung
post_type = db.Column(db.String(20), default='text') # text, image, video, link, thought_share
visibility = db.Column(db.String(20), default='public') # public, friends, private
is_pinned = db.Column(db.Boolean, default=False)
like_count = db.Column(db.Integer, default=0)
comment_count = db.Column(db.Integer, default=0)
share_count = db.Column(db.Integer, default=0)
view_count = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Verknüpfung zu Gedanken (falls der Post einen Gedanken teilt)
shared_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
shared_thought = db.relationship('Thought', backref='shared_in_posts')
# Verknüpfung zu Mindmap-Knoten
shared_node_id = db.Column(db.Integer, db.ForeignKey('mind_map_node.id'), nullable=True)
shared_node = db.relationship('MindMapNode', backref='shared_in_posts')
# Kommentare zu diesem Post
comments = db.relationship('SocialComment', backref='post', lazy=True, cascade="all, delete-orphan")
def __repr__(self):
return f'<SocialPost {self.id} by {self.author.username}>'
def to_dict(self):
return {
'id': self.id,
'content': self.content,
'post_type': self.post_type,
'image_url': self.image_url,
'video_url': self.video_url,
'link_url': self.link_url,
'link_title': self.link_title,
'link_description': self.link_description,
'visibility': self.visibility,
'is_pinned': self.is_pinned,
'like_count': self.like_count,
'comment_count': self.comment_count,
'share_count': self.share_count,
'view_count': self.view_count,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'author': {
'id': self.author.id,
'username': self.author.username,
'display_name': self.author.display_name or self.author.username,
'avatar': self.author.avatar,
'is_verified': self.author.is_verified
},
'shared_thought': self.shared_thought.to_dict() if self.shared_thought else None,
'shared_node': self.shared_node.to_dict() if self.shared_node else None
}
class SocialComment(db.Model):
"""Kommentare zu Posts"""
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text, nullable=False)
like_count = db.Column(db.Integer, default=0)
reply_count = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
# Antworten auf diesen Kommentar
replies = db.relationship('SocialComment', backref=db.backref('parent', remote_side=[id]), lazy=True)
def __repr__(self):
return f'<SocialComment {self.id} by {self.author.username}>'
def to_dict(self):
return {
'id': self.id,
'content': self.content,
'like_count': self.like_count,
'reply_count': self.reply_count,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'author': {
'id': self.author.id,
'username': self.author.username,
'display_name': self.author.display_name or self.author.username,
'avatar': self.author.avatar,
'is_verified': self.author.is_verified
},
'parent_id': self.parent_id
}
class Notification(db.Model):
"""Benachrichtigungen für Benutzer"""
id = db.Column(db.Integer, primary_key=True)
type = db.Column(db.String(50), nullable=False) # follow, like, comment, mention, friend_request, etc.
message = db.Column(db.String(500), nullable=False)
is_read = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Verknüpfungen zu anderen Entitäten
related_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
related_comment_id = db.Column(db.Integer, db.ForeignKey('social_comment.id'), nullable=True)
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
# Beziehungen
related_user = db.relationship('User', foreign_keys=[related_user_id])
related_post = db.relationship('SocialPost', foreign_keys=[related_post_id])
related_comment = db.relationship('SocialComment', foreign_keys=[related_comment_id])
related_thought = db.relationship('Thought', foreign_keys=[related_thought_id])
def __repr__(self):
return f'<Notification {self.id} for {self.user.username}>'
def to_dict(self):
return {
'id': self.id,
'type': self.type,
'message': self.message,
'is_read': self.is_read,
'created_at': self.created_at.isoformat(),
'related_user': self.related_user.username if self.related_user else None,
'related_post_id': self.related_post_id,
'related_comment_id': self.related_comment_id,
'related_thought_id': self.related_thought_id
}
class UserSettings(db.Model):
"""Benutzereinstellungen"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True)
# Datenschutz-Einstellungen
profile_visibility = db.Column(db.String(20), default='public') # public, friends, private
show_email = db.Column(db.Boolean, default=False)
show_birth_date = db.Column(db.Boolean, default=False)
show_location = db.Column(db.Boolean, default=True)
allow_friend_requests = db.Column(db.Boolean, default=True)
allow_messages = db.Column(db.String(20), default='everyone') # everyone, friends, none
# Benachrichtigungs-Einstellungen
email_notifications = db.Column(db.Boolean, default=True)
push_notifications = db.Column(db.Boolean, default=True)
notify_on_follow = db.Column(db.Boolean, default=True)
notify_on_like = db.Column(db.Boolean, default=True)
notify_on_comment = db.Column(db.Boolean, default=True)
notify_on_mention = db.Column(db.Boolean, default=True)
notify_on_friend_request = db.Column(db.Boolean, default=True)
# Interface-Einstellungen
dark_mode = db.Column(db.Boolean, default=False)
language = db.Column(db.String(10), default='de')
# Beziehung
user = db.relationship('User', backref=db.backref('settings', uselist=False))
def __repr__(self):
return f'<UserSettings for {self.user.username}>'
class Activity(db.Model):
"""Aktivitätsprotokoll für Benutzer"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
action = db.Column(db.String(100), nullable=False) # login, logout, post_created, thought_shared, etc.
description = db.Column(db.String(500), nullable=True)
ip_address = db.Column(db.String(45), nullable=True) # IPv4/IPv6
user_agent = db.Column(db.String(500), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Verknüpfungen zu anderen Entitäten
related_post_id = db.Column(db.Integer, db.ForeignKey('social_post.id'), nullable=True)
related_thought_id = db.Column(db.Integer, db.ForeignKey('thought.id'), nullable=True)
related_mindmap_id = db.Column(db.Integer, db.ForeignKey('user_mindmap.id'), nullable=True)
# Beziehungen
user = db.relationship('User', backref='activities')
related_post = db.relationship('SocialPost')
related_thought = db.relationship('Thought')
related_mindmap = db.relationship('UserMindmap')
def __repr__(self):
return f'<Activity {self.action} by {self.user.username}>'

View File

@@ -11,4 +11,7 @@ gunicorn==21.2.0
#pillow==10.0.1
pytest==7.4.0
pytest-flask==1.2.0
Flask-Migrate
Flask-Migrate
flask-socketio==5.3.6
python-engineio==4.8.2
python-socketio==5.11.1

22
server.log Normal file
View File

@@ -0,0 +1,22 @@
⏰ 21:58:48.486 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet
⏰ 21:58:48.486 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
⏰ 21:58:49.951 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 OpenAI API-Verbindung erfolgreich hergestellt
⏰ 21:58:50.122 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbank erfolgreich initialisiert
⏰ 21:58:50.132 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbanktabellen erstellt/aktualisiert
⏰ 21:58:50.134 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with watchdog (inotify)
⏰ 21:58:52.225 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet
⏰ 21:58:52.226 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
⏰ 21:58:53.848 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 OpenAI API-Verbindung erfolgreich hergestellt
⏰ 21:58:53.997 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbank erfolgreich initialisiert
⏰ 21:58:54.002 │ ✅ INFO  │ 🗄 [DB ] │ 🚫 Datenbanktabellen erstellt/aktualisiert
⏰ 21:58:54.006 │ ✅ INFO  │ ⚙ [SYSTEM ] │ 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
* Debugger is active!
* Debugger PIN: 114-005-893

53
start.sh Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env powershell
# Windows PowerShell-Version des Start-Skripts
# Datum: 01.05.2025
# Docker-Status prüfen
Write-Host "Prüfe Docker-Status..." -ForegroundColor Cyan
try {
$status = docker ps -q
if ($LASTEXITCODE -ne 0) {
Write-Host "Docker ist nicht gestartet. Bitte starten Sie Docker Desktop." -ForegroundColor Red
exit 1
}
} catch {
Write-Host "Docker ist nicht verfügbar. Bitte installieren Sie Docker Desktop und starten Sie es." -ForegroundColor Red
Write-Host $_.Exception.Message
exit 1
}
# Alte Container stoppen und entfernen
$containerExists = docker ps -a --filter "name=systades_app" -q
if ($containerExists) {
Write-Host "Stoppe und entferne alten Container..." -ForegroundColor Yellow
docker rm -f systades_app
}
# Alte Images löschen
Write-Host "Entferne altes Image..." -ForegroundColor Yellow
docker rmi -f systades_app:latest
# Stelle sicher, dass das Datenbankverzeichnis existiert
if (-not (Test-Path "database")) {
New-Item -Path "database" -ItemType Directory -Force
}
# Docker-Compose Setup neu bauen
Write-Host "Baue Container neu..." -ForegroundColor Green
docker-compose build --no-cache
# Docker-Compose neu starten
Write-Host "Starte Container..." -ForegroundColor Green
docker-compose up -d --force-recreate
# Warte kurz und prüfe, ob der Container läuft
Write-Host "Prüfe Container-Status..." -ForegroundColor Cyan
Start-Sleep -Seconds 3
docker ps | Select-String "systades_app"
# Ausgabe
Write-Host "`nSystemstatus:" -ForegroundColor Cyan
Write-Host "----------------------------------------"
Write-Host "Systades-Anwendung ist jetzt unter http://localhost:5000 erreichbar." -ForegroundColor Green
Write-Host "Container-Logs können mit 'docker logs -f systades_app' angezeigt werden." -ForegroundColor Green
Write-Host "----------------------------------------"

View File

@@ -0,0 +1 @@

View File

@@ -1,6 +1,9 @@
/* ChatGPT Assistent Styles - Verbesserte Version */
#chatgpt-assistant {
font-family: 'Inter', sans-serif;
bottom: 5.5rem;
z-index: 100;
max-height: 85vh;
}
#assistant-chat {
@@ -10,6 +13,15 @@
border-radius: 0.75rem;
overflow: hidden;
max-width: calc(100vw - 2rem);
max-height: 80vh !important;
}
#assistant-history {
max-height: calc(80vh - 150px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
padding-bottom: 2rem; /* Zusätzlicher Abstand unten */
}
#assistant-toggle {
@@ -22,11 +34,6 @@
transform: scale(1.1) rotate(10deg);
}
#assistant-history {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
#assistant-history::-webkit-scrollbar {
width: 6px;
}
@@ -142,14 +149,21 @@
.typing-indicator span {
height: 8px;
width: 8px;
background-color: #888;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
opacity: 0.4;
opacity: 0.6;
animation: bounce 1.4s infinite ease-in-out;
}
body.dark .typing-indicator span {
background-color: rgba(255, 255, 255, 0.7);
}
body:not(.dark) .typing-indicator span {
background-color: rgba(107, 114, 128, 0.8);
}
.typing-indicator span:nth-child(1) { animation-delay: 0s; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@@ -173,11 +187,12 @@
@media (max-width: 640px) {
#assistant-chat {
width: calc(100vw - 2rem) !important;
max-height: 65vh !important;
}
#chatgpt-assistant {
right: 1rem;
bottom: 1rem;
bottom: 6rem;
}
}
@@ -200,4 +215,38 @@ main {
footer {
flex-shrink: 0;
}
/* Verbesserte Farbkontraste für Nachrichtenblasen */
.user-message {
background-color: rgba(124, 58, 237, 0.1) !important;
color: #4B5563 !important;
}
body.dark .user-message {
background-color: rgba(124, 58, 237, 0.2) !important;
color: #F9FAFB !important;
}
.assistant-message {
background-color: #F3F4F6 !important;
color: #1F2937 !important;
border-left: 3px solid #8B5CF6;
}
body.dark .assistant-message {
background-color: rgba(31, 41, 55, 0.5) !important;
color: #F9FAFB !important;
border-left: 3px solid #8B5CF6;
}
/* Chat-Assistent-Position im Footer-Bereich anpassen */
.chat-assistant {
max-height: 75vh;
bottom: 1.5rem;
}
.chat-assistant .chat-messages {
max-height: calc(75vh - 180px);
overflow-y: auto;
}

View File

@@ -40,8 +40,8 @@
--light-bg: #f9fafb;
--light-text: #1e293b;
--light-heading: #0f172a;
--light-primary: #3b82f6;
--light-primary-hover: #4f46e5;
--light-primary: #7c3aed;
--light-primary-hover: #6d28d9;
--light-secondary: #6b7280;
--light-border: #e5e7eb;
--light-card-bg: rgba(255, 255, 255, 0.92);
@@ -68,18 +68,37 @@ body {
line-height: 1.5;
}
/* Dark Mode */
html.dark body {
/* Strikte Trennung: Dark Mode */
html.dark body,
body.dark {
background-color: var(--bg-primary-dark);
color: var(--text-primary-dark);
}
/* Light Mode */
/* Strikte Trennung: Light Mode */
html:not(.dark) body,
body:not(.dark) {
background-color: var(--light-bg);
color: var(--light-text);
}
/* Verbesserte Trennung: Container und Karten */
body.dark .card,
body.dark .glass-card,
body.dark .panel {
background-color: var(--bg-secondary-dark);
border-color: var(--border-dark);
color: var(--text-primary-dark);
}
body:not(.dark) .card,
body:not(.dark) .glass-card,
body:not(.dark) .panel {
background-color: var(--light-card-bg);
border-color: var(--light-border);
color: var(--light-text);
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
@@ -388,7 +407,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
}
.section-heading {
font-size: 1.5rem;
font-size: 1.75rem;
}
}
@@ -455,22 +474,60 @@ body:not(.dark) a:hover {
}
/* Light Mode Buttons */
body:not(.dark) button:not(.toggle):not(.plain-btn) {
color: white !important;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
body:not(.dark) .btn,
body:not(.dark) button:not(.toggle) {
background-color: var(--light-primary);
color: white;
border: none;
box-shadow: var(--light-shadow);
border-radius: 0.375rem;
padding: 0.5rem 1rem;
transition: all 0.3s ease;
body:not(.dark) .btn-primary,
body:not(.dark) .btn-secondary,
body:not(.dark) .btn-success,
body:not(.dark) .btn-danger,
body:not(.dark) .btn-warning,
body:not(.dark) .btn-info {
color: white !important;
}
body:not(.dark) .btn:hover,
body:not(.dark) button:not(.toggle):hover {
background-color: var(--light-primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #7c3aed, #6d28d9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.3);
}
/* Dark/Light Mode Switch Button */
.theme-toggle {
position: relative;
width: 48px;
height: 24px;
background: linear-gradient(to right, #7c3aed, #3b82f6);
border-radius: 24px;
padding: 2px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.theme-toggle::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.theme-toggle.dark::after {
transform: translateX(24px);
}
.theme-toggle:hover::after {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
/* Light Mode Cards und Panels */
@@ -523,4 +580,489 @@ body:not(.dark) .navbar {
background-color: var(--light-navbar-bg);
box-shadow: var(--light-shadow);
border-bottom: 1px solid var(--light-border);
}
/* Erweiterte Light-Mode-spezifische Stile */
body:not(.dark) .glass-effect {
background-color: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(209, 213, 219, 0.3);
}
body:not(.dark) .card {
background-color: rgba(255, 255, 255, 0.85);
border: 1px solid var(--light-border);
box-shadow: var(--light-shadow);
transition: all 0.3s ease;
}
body:not(.dark) .card:hover {
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
/* Light Mode Buttons mit verbesserter Lesbarkeit */
body:not(.dark) .btn-primary {
background: linear-gradient(135deg, #6d28d9, #5b21b6);
color: white;
border: none;
transition: all 0.2s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
font-weight: 600;
letter-spacing: 0.02em;
box-shadow: 0 2px 4px rgba(91, 33, 182, 0.3);
border-radius: 8px;
}
body:not(.dark) .btn-primary:hover {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
body:not(.dark) .btn-secondary {
background: linear-gradient(135deg, #ffffff, #f9fafb);
color: #1f2937;
border: 2px solid #e5e7eb;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
border-radius: 8px;
}
body:not(.dark) .btn-secondary:hover {
background: linear-gradient(135deg, #f9fafb, #f3f4f6);
border-color: #d1d5db;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
}
body:not(.dark) .btn-outline {
background-color: transparent;
color: var(--light-primary);
border: 1px solid var(--light-primary);
}
body:not(.dark) .btn-outline:hover {
background-color: rgba(124, 58, 237, 0.05);
}
/* Light Mode Formulare */
body:not(.dark) input,
body:not(.dark) select,
body:not(.dark) textarea {
background-color: white;
border: 1px solid #d1d5db;
color: #1f2937;
}
body:not(.dark) input:focus,
body:not(.dark) select:focus,
body:not(.dark) textarea:focus {
border-color: var(--light-primary);
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
}
/* Light Mode Navigation */
body:not(.dark) .sidebar {
background-color: white;
border-right: 1px solid #e5e7eb;
}
body:not(.dark) .sidebar-link {
color: #4b5563;
}
body:not(.dark) .sidebar-link:hover {
background-color: #f3f4f6;
color: var(--light-primary);
}
body:not(.dark) .sidebar-link.active {
background-color: rgba(124, 58, 237, 0.08);
color: var(--light-primary);
font-weight: 500;
}
/* Light Mode Tabellen */
body:not(.dark) table {
border-color: #e5e7eb;
}
body:not(.dark) th {
background-color: #f9fafb;
color: #111827;
font-weight: 600;
}
body:not(.dark) tr:nth-child(even) {
background-color: #f9fafb;
}
body:not(.dark) tr:hover {
background-color: #f3f4f6;
}
/* Light Mode Icons */
body:not(.dark) .icon {
color: #6b7280;
}
body:not(.dark) .icon-primary {
color: var(--light-primary);
}
/* Light Mode Alerts/Benachrichtigungen */
body:not(.dark) .alert-info {
background-color: #eff6ff;
border-left: 4px solid #3b82f6;
color: #1e40af;
}
body:not(.dark) .alert-success {
background-color: #ecfdf5;
border-left: 4px solid #10b981;
color: #065f46;
}
body:not(.dark) .alert-warning {
background-color: #fffbeb;
border-left: 4px solid #f59e0b;
color: #92400e;
}
body:not(.dark) .alert-error {
background-color: #fef2f2;
border-left: 4px solid #ef4444;
color: #b91c1c;
}
/* Light Mode Badge */
body:not(.dark) .badge {
background-color: #e5e7eb;
color: #374151;
}
body:not(.dark) .badge-primary {
background-color: rgba(124, 58, 237, 0.1);
color: var(--light-primary);
}
/* Light Mode Mindmap spezifisch */
body:not(.dark) #cy {
background-color: rgba(255, 255, 255, 0.7);
border: 1px solid #e5e7eb;
}
body:not(.dark) .node {
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
body:not(.dark) .node:hover,
body:not(.dark) .node.selected {
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.5), 0 4px 8px rgba(0, 0, 0, 0.1);
}
body:not(.dark) .edge {
opacity: 0.7;
}
body:not(.dark) .edge:hover,
body:not(.dark) .edge.selected {
opacity: 1;
}
/* Footer im Light Mode */
body:not(.dark) footer {
background-color: rgba(249, 250, 251, 0.7);
border-top: 1px solid #e5e7eb;
}
/* Alpine.js Transitions im Light Mode */
body:not(.dark) [x-cloak] {
display: none !important;
}
/* Suchfeldstyling im Light Mode */
body:not(.dark) .search-container input {
background-color: white;
border: 1px solid #d1d5db;
color: #1f2937;
}
body:not(.dark) .search-container input:focus {
border-color: var(--light-primary);
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
}
body:not(.dark) .search-results {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .search-result-item:hover {
background-color: #f3f4f6;
}
/* Profile und Benutzermenü im Light Mode */
body:not(.dark) .avatar {
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
body:not(.dark) .user-dropdown {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .user-dropdown-item:hover {
background-color: #f3f4f6;
}
/* Medienabfragen für Responsivität */
@media (max-width: 640px) {
/* Optimierungen für Smartphones */
body {
padding-left: 0;
padding-right: 0;
}
.container {
padding-left: 1rem;
padding-right: 1rem;
}
.hero-heading {
font-size: 2rem;
}
.section-heading {
font-size: 1.75rem;
}
.card, .panel, .glass-card {
padding: 1rem;
border-radius: 10px;
}
.navbar {
padding: 0.75rem 1rem;
}
/* Optimierte Touch-Ziele für mobile Geräte */
button, .btn, .nav-link, .menu-item {
min-height: 44px;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
}
/* Verbesserte Lesbarkeit auf kleinen Bildschirmen */
p, li, input, textarea, button, .text-sm {
font-size: 1rem;
line-height: 1.5;
}
/* Anpassungen für Tabellen auf kleinen Bildschirmen */
table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
/* Optimierte Formulare */
input, select, textarea {
font-size: 16px; /* Verhindert iOS-Zoom bei Fokus */
width: 100%;
}
/* Verbesserter Abstand für Touch-Targets */
nav a, nav button, .menu-item {
margin: 0.25rem 0;
padding: 0.75rem;
}
}
@media (min-width: 641px) and (max-width: 1024px) {
/* Optimierungen für Tablets */
.container {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
/* Zweispaltige Layouts für mittlere Bildschirme */
.grid-cols-1 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
/* Optimierte Navigationsleiste */
.navbar {
padding: 0.75rem 1.5rem;
}
}
@media (min-width: 1025px) {
/* Optimierungen für Desktop */
.container {
padding-left: 2rem;
padding-right: 2rem;
max-width: 1280px;
margin: 0 auto;
}
/* Mehrspaltige Layouts für große Bildschirme */
.grid-cols-1 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
/* Hover-Effekte nur auf Desktop-Geräten */
.card:hover, .panel:hover, .glass-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* Desktop-spezifische Animationen */
.animate-hover {
transition: all 0.3s ease;
}
.animate-hover:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
}
/* Responsive design improvements */
.responsive-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.responsive-flex {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.responsive-flex > * {
flex: 1 1 280px;
}
/* Accessibility improvements */
.focus-visible:focus-visible {
outline: 2px solid var(--accent-primary-light);
outline-offset: 2px;
}
body.dark .focus-visible:focus-visible {
outline: 2px solid var(--accent-primary-dark);
}
/* Print styles */
@media print {
body {
background: white !important;
color: black !important;
}
nav, footer, button, .no-print {
display: none !important;
}
main, article, .card, .panel, .container {
width: 100% !important;
border: none !important;
box-shadow: none !important;
color: black !important;
background: white !important;
}
a {
color: black !important;
text-decoration: underline !important;
}
a::after {
content: " (" attr(href) ")";
font-size: 0.8em;
font-weight: normal;
}
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
page-break-inside: avoid;
}
img {
page-break-inside: avoid;
max-width: 100% !important;
}
table {
page-break-inside: avoid;
}
@page {
margin: 2cm;
}
}
/* Light Mode KI-Chatfenster */
body:not(.dark) .chat-container {
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
color: #1e293b;
}
body:not(.dark) .chat-message-ai {
background-color: rgba(124, 58, 237, 0.1);
border: 1px solid rgba(124, 58, 237, 0.2);
}
body:not(.dark) .chat-message-user {
background-color: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* Anpassung der Chatfenster-Größe */
.chat-assistant {
max-height: 85vh; /* Vergrößert von 80vh */
bottom: 1rem; /* Etwas höher positionieren */
}
.chat-assistant .chat-messages {
max-height: calc(85vh - 180px); /* Angepasst für größeres Fenster */
overflow-y: auto;
padding-bottom: 2rem; /* Zusätzlicher Abstand um Abschneiden zu vermeiden */
}
/* Verbesserungen für das Mobilmenü */
@media (max-width: 768px) {
.mobile-menu-container {
max-height: 85vh;
overflow-y: auto;
}
#chatgpt-assistant {
bottom: 4.5rem !important;
}
.chat-assistant {
max-height: 70vh !important;
}
.chat-assistant .chat-messages {
max-height: calc(70vh - 160px) !important;
}
}

437
static/css/mindmap.css Normal file
View File

@@ -0,0 +1,437 @@
/* Mindmap Container Styles */
.mindmap-container {
position: relative;
width: 100%;
height: 100%;
min-height: 600px;
background: var(--bg-primary);
border-radius: 12px;
overflow: hidden;
}
/* Cytoscape Container für die Hauptmindmap */
#cy {
width: 100%;
height: 100%;
background: transparent;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
/* Subpage Styles - Identisches Design wie Hauptmindmap */
.mindmap-subpage {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
display: flex;
flex-direction: column;
z-index: 10;
transition: opacity 0.3s ease, transform 0.3s ease;
opacity: 1;
transform: translateY(0);
}
/* Subpage Header */
.subpage-header {
display: flex;
align-items: center;
padding: 16px;
background: var(--bg-secondary);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
z-index: 2;
}
.dark .subpage-header {
background: rgba(30, 41, 59, 0.8);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* Zurück-Button */
.back-button {
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
margin-right: 12px;
}
.back-button:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
/* Subpage Titel */
.subpage-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin: 0;
background: linear-gradient(90deg, #60a5fa, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Subpage Cytoscape Container */
.subpage-cy-container {
position: relative;
flex: 1;
width: 100%;
height: calc(100% - 72px);
overflow: hidden;
z-index: 1;
}
/* Toolbar für Zoom-Kontrollen */
.mindmap-toolbar {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
padding: 8px;
background: rgba(30, 41, 59, 0.8);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
z-index: 20;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mindmap-toolbar button {
width: 40px;
height: 40px;
border: none;
background: rgba(255, 255, 255, 0.1);
color: white;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.mindmap-toolbar button:hover {
background: rgba(139, 92, 246, 0.5);
transform: translateY(-2px);
}
.mindmap-toolbar button i {
font-size: 16px;
}
/* Mindmap Header */
.mindmap-header {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 1.5rem;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Dark Mode spezifische Stile */
.dark .mindmap-subpage {
background: linear-gradient(135deg, #0f172a 0%, #0c1221 100%);
}
/* Fix für Zoom-Buttons */
body.dark .mindmap-toolbar button {
background: rgba(255, 255, 255, 0.1);
color: white;
}
body:not(.dark) .mindmap-toolbar button {
background: rgba(30, 41, 59, 0.2);
color: #1e293b;
}
/* Kontext-Menü-Anpassungen */
.context-menu {
z-index: 1000;
}
/* Export Group Styles */
.export-group {
position: relative;
}
.export-options {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
padding: 8px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: none;
flex-direction: column;
gap: 4px;
min-width: 160px;
}
.dark .export-options {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.export-group:hover .export-options {
display: flex;
}
.export-options button {
width: 100%;
height: auto;
padding: 8px 12px;
justify-content: flex-start;
font-size: 14px;
border-radius: 6px;
}
/* Context Menu Styles */
.mindmap-context-menu {
position: fixed;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 8px;
z-index: 1000;
min-width: 180px;
}
.dark .mindmap-context-menu {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mindmap-context-menu button {
display: flex;
align-items: center;
width: 100%;
padding: 8px 12px;
border: none;
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s ease;
}
.mindmap-context-menu button:hover {
background: var(--accent-primary);
color: white;
}
.mindmap-context-menu button i {
margin-right: 8px;
width: 16px;
}
/* Node Styles */
.mindmap-node {
background-color: var(--bg-secondary);
border: 2px solid var(--accent-primary);
border-radius: 8px;
padding: 8px 12px;
transition: all 0.3s ease;
}
.mindmap-node:hover {
box-shadow: 0 0 0 2px var(--accent-primary);
transform: scale(1.05);
}
.mindmap-node.selected {
border-color: var(--accent-secondary);
box-shadow: 0 0 0 3px var(--accent-secondary);
}
/* Edge Styles */
.mindmap-edge {
width: 2px;
transition: all 0.3s ease;
}
.dark .mindmap-edge {
background-color: rgba(255, 255, 255, 0.2);
}
.mindmap-edge:hover {
width: 3px;
background-color: var(--accent-primary);
}
/* Animation Styles */
@keyframes nodeAppear {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.mindmap-node-new {
animation: nodeAppear 0.3s ease forwards;
}
/* Responsive Styles */
@media (max-width: 768px) {
.mindmap-toolbar {
flex-wrap: wrap;
width: calc(100% - 32px);
justify-content: center;
}
.export-options {
left: 0;
right: auto;
}
}
/* Loading State */
.mindmap-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.mindmap-loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--bg-secondary);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Tooltip Styles */
.mindmap-tooltip {
position: absolute;
background: var(--bg-secondary);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dark .mindmap-tooltip {
background: rgba(30, 41, 59, 0.9);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* Kategorien-Panel */
.categories-panel {
position: absolute;
top: 80px;
left: 20px;
width: 300px;
max-height: calc(100vh - 120px);
background: rgba(15, 23, 42, 0.95);
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 1000;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transform: translateX(-320px);
transition: transform 0.3s ease-in-out;
overflow-y: auto;
}
.categories-panel.visible {
transform: translateX(0);
}
.categories-panel h3 {
color: white;
font-size: 1.2rem;
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.category-list {
list-style: none;
padding: 0;
margin: 0;
}
.category-item {
display: flex;
align-items: center;
padding: 8px 12px;
margin: 4px 0;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
color: white;
}
.category-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(4px);
}
.category-color {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 12px;
flex-shrink: 0;
}
.category-name {
flex-grow: 1;
font-size: 0.95rem;
}
.category-count {
background: rgba(255, 255, 255, 0.1);
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
margin-left: 8px;
}

915
static/css/social.css Normal file
View File

@@ -0,0 +1,915 @@
/* ================================
SysTades Social Network Styles
================================ */
:root {
/* Primary Colors */
--primary-50: #f0f9ff;
--primary-100: #e0f2fe;
--primary-200: #bae6fd;
--primary-300: #7dd3fc;
--primary-400: #38bdf8;
--primary-500: #0ea5e9;
--primary-600: #0284c7;
--primary-700: #0369a1;
--primary-800: #075985;
--primary-900: #0c4a6e;
/* Neutral Colors */
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
/* Semantic Colors */
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
/* Social Media Colors */
--like-color: #ec4899;
--share-color: #8b5cf6;
--bookmark-color: #f59e0b;
--comment-color: var(--primary-500);
/* Glassmorphism */
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
/* Animations */
--transition-fast: 0.15s ease-out;
--transition-normal: 0.3s ease-out;
--transition-slow: 0.6s ease-out;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* Border Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
}
/* Dark Mode Variables */
[data-theme="dark"] {
--glass-bg: rgba(0, 0, 0, 0.1);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
/* ================================
Performance Optimizations
================================ */
* {
box-sizing: border-box;
}
img {
max-width: 100%;
height: auto;
}
/* GPU Acceleration for animations */
.accelerated {
transform: translateZ(0);
will-change: transform;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* ================================
Social Feed Styles
================================ */
.social-feed {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.post-card {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
margin-bottom: 1.5rem;
padding: 1.5rem;
box-shadow: var(--shadow-lg);
transition: all var(--transition-normal);
position: relative;
overflow: hidden;
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-xl);
border-color: var(--primary-300);
}
.post-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary-500), var(--primary-600));
opacity: 0;
transition: opacity var(--transition-normal);
}
.post-card:hover::before {
opacity: 1;
}
/* Post Header */
.post-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 0.75rem;
}
.post-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
border: 2px solid var(--primary-500);
transition: all var(--transition-fast);
cursor: pointer;
}
.post-avatar:hover {
transform: scale(1.1);
border-color: var(--primary-400);
}
.post-author {
flex: 1;
}
.post-author-name {
font-weight: 600;
color: var(--gray-800);
margin: 0;
font-size: 1rem;
}
.post-author-username {
color: var(--gray-500);
font-size: 0.875rem;
margin: 0;
}
.post-time {
color: var(--gray-400);
font-size: 0.875rem;
}
/* Post Content */
.post-content {
margin-bottom: 1rem;
line-height: 1.6;
color: var(--gray-700);
}
.post-type-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: var(--radius-md);
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.post-type-text { background: var(--gray-100); color: var(--gray-600); }
.post-type-thought { background: var(--primary-100); color: var(--primary-600); }
.post-type-question { background: var(--warning); color: white; }
.post-type-insight { background: var(--success); color: white; }
/* Post Actions */
.post-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid var(--gray-200);
}
.action-group {
display: flex;
gap: 1rem;
}
.action-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: none;
background: transparent;
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
font-size: 0.875rem;
color: var(--gray-500);
}
.action-btn:hover {
background: var(--gray-100);
color: var(--gray-700);
transform: translateY(-1px);
}
.action-btn.active {
color: var(--primary-600);
background: var(--primary-50);
}
.action-btn i {
font-size: 1rem;
}
/* Specific action colors */
.action-btn.like-btn.active {
color: var(--like-color);
background: rgba(236, 72, 153, 0.1);
}
.action-btn.share-btn:hover {
color: var(--share-color);
background: rgba(139, 92, 246, 0.1);
}
.action-btn.bookmark-btn.active {
color: var(--bookmark-color);
background: rgba(245, 158, 11, 0.1);
}
/* ================================
Comments Section
================================ */
.comments-section {
border-top: 1px solid var(--gray-200);
padding-top: 1rem;
margin-top: 1rem;
}
.comment-item {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
padding: 0.75rem;
border-radius: var(--radius-lg);
background: var(--gray-50);
transition: background var(--transition-fast);
}
.comment-item:hover {
background: var(--gray-100);
}
.comment-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid var(--primary-400);
}
.comment-content {
flex: 1;
}
.comment-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.comment-author {
font-weight: 600;
color: var(--gray-800);
font-size: 0.875rem;
}
.comment-time {
color: var(--gray-400);
font-size: 0.75rem;
}
.comment-text {
color: var(--gray-700);
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 0.5rem;
}
.comment-actions {
display: flex;
gap: 1rem;
}
.comment-action {
background: none;
border: none;
color: var(--gray-400);
font-size: 0.75rem;
cursor: pointer;
transition: color var(--transition-fast);
}
.comment-action:hover {
color: var(--primary-500);
}
/* Comment Form */
.comment-form {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.comment-form textarea {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-lg);
resize: none;
min-height: 80px;
transition: border-color var(--transition-fast);
}
.comment-form textarea:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
}
.comment-submit {
padding: 0.75rem 1.5rem;
background: var(--primary-500);
color: white;
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-fast);
font-weight: 500;
}
.comment-submit:hover {
background: var(--primary-600);
transform: translateY(-1px);
}
/* ================================
Create Post Form
================================ */
.create-post-form {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-md);
}
.create-post-textarea {
width: 100%;
min-height: 120px;
padding: 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-lg);
resize: vertical;
font-family: inherit;
font-size: 1rem;
line-height: 1.5;
transition: all var(--transition-normal);
}
.create-post-textarea:focus {
outline: none;
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
}
.create-post-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
gap: 1rem;
}
.post-type-select,
.post-visibility-select {
padding: 0.5rem 1rem;
border: 1px solid var(--gray-300);
border-radius: var(--radius-md);
background: white;
cursor: pointer;
transition: border-color var(--transition-fast);
}
.post-type-select:focus,
.post-visibility-select:focus {
outline: none;
border-color: var(--primary-500);
}
.create-post-btn {
padding: 0.75rem 2rem;
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
color: white;
border: none;
border-radius: var(--radius-lg);
cursor: pointer;
font-weight: 600;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.create-post-btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.create-post-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* ================================
Filter Tabs
================================ */
.feed-filters {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
padding: 0.5rem;
background: var(--gray-100);
border-radius: var(--radius-xl);
overflow-x: auto;
}
.filter-tab {
padding: 0.75rem 1.5rem;
border: none;
background: transparent;
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition-fast);
font-weight: 500;
white-space: nowrap;
color: var(--gray-600);
}
.filter-tab:hover {
background: var(--gray-200);
color: var(--gray-800);
}
.filter-tab.active {
background: var(--primary-500);
color: white;
box-shadow: var(--shadow-md);
}
/* ================================
Notifications
================================ */
.notification-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: var(--radius-lg);
transition: all var(--transition-fast);
cursor: pointer;
position: relative;
}
.notification-item:hover {
background: var(--gray-50);
transform: translateX(4px);
}
.notification-item.unread {
background: var(--primary-50);
border-left: 4px solid var(--primary-500);
}
.notification-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: white;
}
.notification-like { background: var(--like-color); }
.notification-comment { background: var(--comment-color); }
.notification-follow { background: var(--success); }
.notification-share { background: var(--share-color); }
.notification-content {
flex: 1;
}
.notification-text {
margin: 0 0 0.25rem 0;
color: var(--gray-800);
font-size: 0.875rem;
}
.notification-time {
color: var(--gray-500);
font-size: 0.75rem;
}
.notification-actions {
display: flex;
gap: 0.5rem;
}
.notification-delete {
background: none;
border: none;
color: var(--gray-400);
cursor: pointer;
padding: 0.5rem;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.notification-delete:hover {
background: var(--error);
color: white;
}
/* ================================
User Profile
================================ */
.profile-header {
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
color: white;
padding: 2rem;
border-radius: var(--radius-2xl);
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.profile-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.profile-info {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 1.5rem;
}
.profile-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.3);
box-shadow: var(--shadow-xl);
}
.profile-details h1 {
margin: 0 0 0.5rem 0;
font-size: 2rem;
font-weight: 700;
}
.profile-username {
opacity: 0.9;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.profile-bio {
opacity: 0.8;
line-height: 1.5;
max-width: 500px;
}
.profile-stats {
display: flex;
gap: 2rem;
margin-top: 1.5rem;
}
.stat-item {
text-align: center;
}
.stat-number {
display: block;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
opacity: 0.8;
}
.follow-btn {
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 0.75rem 2rem;
border-radius: var(--radius-lg);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-normal);
backdrop-filter: blur(10px);
}
.follow-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.follow-btn.following {
background: rgba(255, 255, 255, 0.9);
color: var(--primary-600);
}
/* Profile Tabs */
.profile-tabs {
display: flex;
border-bottom: 1px solid var(--gray-200);
margin-bottom: 2rem;
overflow-x: auto;
}
.profile-tab {
padding: 1rem 2rem;
border: none;
background: none;
cursor: pointer;
position: relative;
color: var(--gray-600);
font-weight: 500;
transition: all var(--transition-fast);
white-space: nowrap;
}
.profile-tab:hover {
color: var(--primary-600);
}
.profile-tab.active {
color: var(--primary-600);
}
.profile-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--primary-500);
border-radius: 2px 2px 0 0;
}
/* ================================
Responsive Design
================================ */
@media (max-width: 768px) {
.social-feed {
padding: 0.5rem;
}
.post-card {
padding: 1rem;
margin-bottom: 1rem;
}
.post-actions {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.action-group {
justify-content: space-around;
}
.create-post-options {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.feed-filters {
padding: 0.25rem;
gap: 0.25rem;
}
.filter-tab {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.profile-header {
padding: 1.5rem;
}
.profile-info {
flex-direction: column;
text-align: center;
gap: 1rem;
}
.profile-avatar {
width: 80px;
height: 80px;
}
.profile-details h1 {
font-size: 1.5rem;
}
.profile-stats {
justify-content: center;
gap: 1.5rem;
}
.profile-tabs {
gap: 0;
}
.profile-tab {
flex: 1;
padding: 0.75rem 1rem;
text-align: center;
font-size: 0.875rem;
}
}
/* ================================
Loading & Animations
================================ */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--gray-300);
border-radius: 50%;
border-top-color: var(--primary-500);
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-up {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ================================
Toast Notifications
================================ */
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 1000;
pointer-events: none;
}
.toast {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 1rem 1.5rem;
margin-bottom: 0.5rem;
box-shadow: var(--shadow-lg);
pointer-events: all;
max-width: 400px;
animation: slideInRight 0.3s ease-out;
}
.toast.success {
border-left: 4px solid var(--success);
}
.toast.error {
border-left: 4px solid var(--error);
}
.toast.warning {
border-left: 4px solid var(--warning);
}
.toast.info {
border-left: 4px solid var(--info);
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* ================================
Utilities
================================ */
.text-gradient {
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glass-effect {
background: var(--glass-bg);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
}
.shadow-glow {
box-shadow: 0 0 20px rgba(14, 165, 233, 0.3);
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}

View File

@@ -0,0 +1,11 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -1,25 +1,54 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Generate favicon.ico from SVG using cairosvg and PIL
"""
import os
import io
from cairosvg import svg2png
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')
# Verzeichnis dieses Skripts
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
# SVG zu PNG konvertieren
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
"""Convert SVG to multi-size ICO file"""
img_io = io.BytesIO()
# Höchste Auflösung für Zwischenspeicherung
max_size = max(sizes)
# SVG in PNG konvertieren
with open(svg_path, 'rb') as svg_file:
svg_data = svg_file.read()
svg2png(bytestring=svg_data, write_to=img_io, output_width=max_size, output_height=max_size)
# PNG in verschiedene Größen konvertieren
img = Image.open(img_io)
# Alle Größen für das ICO-Format vorbereiten
img_list = []
for size in sizes:
resized_img = img.resize((size, size), Image.LANCZOS)
img_list.append(resized_img)
# ICO-Datei speichern
img_list[0].save(
ico_path,
format='ICO',
sizes=[(img.width, img.height) for img in img_list],
append_images=img_list[1:]
)
print(f"Favicon {ico_path} wurde erstellt!")
# PNG zu ICO konvertieren
img = Image.open(png_path)
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
# Ursprüngliches Favicon konvertieren
svg_to_ico(
os.path.join(CURRENT_DIR, 'favicon.svg'),
os.path.join(CURRENT_DIR, 'favicon.ico')
)
print(f"Favicon erfolgreich erstellt: {ico_path}")
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
# os.remove(png_path)
# Neues Neuron-Favicon konvertieren
svg_to_ico(
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
)

View File

@@ -0,0 +1,29 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Hintergrund -->
<rect width="32" height="32" rx="8" fill="#6d28d9" />
<!-- Mindmap-Punkte -->
<!-- Zentraler Punkt -->
<circle cx="16" cy="16" r="3.5" fill="#a78bfa" />
<!-- Umgebende Punkte -->
<circle cx="8" cy="10" r="2.5" fill="#8b5cf6" />
<circle cx="24" cy="10" r="2.5" fill="#8b5cf6" />
<circle cx="16" cy="26" r="2.5" fill="#8b5cf6" />
<!-- Verbindende Linien -->
<path d="M16 16 L8 10" stroke="white" stroke-width="1" stroke-linecap="round" />
<path d="M16 16 L24 10" stroke="white" stroke-width="1" stroke-linecap="round" />
<path d="M16 16 L16 26" stroke="white" stroke-width="1" stroke-linecap="round" />
<!-- Weitere Verbindungslinien für mehr Komplexität -->
<path d="M8 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
<path d="M24 10 L16 26" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
<path d="M8 10 L24 10" stroke="#c4b5fd" stroke-width="0.8" stroke-linecap="round" stroke-dasharray="2 1" />
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
<circle cx="5" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
<circle cx="27" cy="20" r="0.8" fill="#ddd6fe" opacity="0.7" />
<circle cx="20" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
<circle cx="12" cy="5" r="0.8" fill="#ddd6fe" opacity="0.7" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,59 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Hintergrund mit Farbverlauf -->
<rect width="64" height="64" rx="16" fill="url(#paint0_linear)" />
<!-- Mindmap-Punkte -->
<!-- Zentraler Punkt -->
<circle cx="32" cy="32" r="8" fill="url(#glow_gradient)" filter="url(#glow)" />
<!-- Umgebende Punkte -->
<circle cx="16" cy="20" r="6" fill="#8b5cf6" />
<circle cx="48" cy="20" r="6" fill="#8b5cf6" />
<circle cx="32" cy="52" r="6" fill="#8b5cf6" />
<circle cx="16" cy="48" r="4" fill="#a78bfa" />
<circle cx="48" cy="48" r="4" fill="#a78bfa" />
<!-- Verbindende Linien (Hauptpfade) -->
<path d="M32 32 L16 20" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L48 20" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L32 52" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L16 48" stroke="white" stroke-width="2" stroke-linecap="round" />
<path d="M32 32 L48 48" stroke="white" stroke-width="2" stroke-linecap="round" />
<!-- Zusätzliche Verbindungslinien -->
<path d="M16 20 L16 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M48 20 L48 48" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M16 20 L48 20" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M16 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<path d="M48 48 L32 52" stroke="#c4b5fd" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 2" />
<!-- Kleine Dekoration-Punkte für Hintergrund-Ähnlichkeit -->
<circle cx="10" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="54" cy="36" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="40" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="24" cy="10" r="1.5" fill="#ddd6fe" opacity="0.7" />
<circle cx="20" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
<circle cx="44" cy="36" r="1.2" fill="#ddd6fe" opacity="0.5" />
<circle cx="32" cy="16" r="1.2" fill="#ddd6fe" opacity="0.5" />
<!-- Definitionen für Farbverläufe und Effekte -->
<defs>
<!-- Haupthintergrund-Farbverlauf -->
<linearGradient id="paint0_linear" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
<stop stop-color="#6d28d9" />
<stop offset="1" stop-color="#4c1d95" />
</linearGradient>
<!-- Glüheffekt für den zentralen Punkt -->
<filter id="glow" x="20" y="20" width="24" height="24" filterUnits="userSpaceOnUse">
<feGaussianBlur stdDeviation="2" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<!-- Farbverlauf für den zentralen Punkt -->
<linearGradient id="glow_gradient" x1="24" y1="24" x2="40" y2="40" gradientUnits="userSpaceOnUse">
<stop stop-color="#a78bfa" />
<stop offset="1" stop-color="#8b5cf6" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

214
static/js/mindmap-init.js Normal file
View File

@@ -0,0 +1,214 @@
/**
* Mindmap Initialisierung und Event-Handling
*/
// Warte auf die Cytoscape-Instanz
document.addEventListener('mindmap-loaded', function() {
const cy = window.cy;
if (!cy) return;
// Event-Listener für Knoten-Klicks
cy.on('tap', 'node', function(evt) {
const node = evt.target;
// Alle vorherigen Hervorhebungen zurücksetzen
cy.nodes().forEach(n => {
n.removeStyle();
n.connectedEdges().removeStyle();
});
// Speichere ausgewählten Knoten
window.mindmapInstance.selectedNode = node;
// Aktiviere leuchtenden Effekt statt Umkreisung
node.style({
'background-opacity': 1,
'background-color': node.data('color'),
'shadow-color': node.data('color'),
'shadow-opacity': 1,
'shadow-blur': 15,
'shadow-offset-x': 0,
'shadow-offset-y': 0
});
// Verbundene Kanten und Knoten hervorheben
const connectedEdges = node.connectedEdges();
const connectedNodes = node.neighborhood('node');
connectedEdges.style({
'line-color': '#a78bfa',
'target-arrow-color': '#a78bfa',
'source-arrow-color': '#a78bfa',
'line-opacity': 0.8,
'width': 2
});
connectedNodes.style({
'shadow-opacity': 0.7,
'shadow-blur': 10,
'shadow-color': '#a78bfa'
});
// Info-Panel aktualisieren
updateInfoPanel(node);
// Seitenleiste aktualisieren
updateSidebar(node);
});
// Klick auf Hintergrund - Auswahl zurücksetzen
cy.on('tap', function(evt) {
if (evt.target === cy) {
resetSelection(cy);
}
});
// Zoom-Controls
document.getElementById('zoomIn')?.addEventListener('click', () => {
cy.zoom({
level: cy.zoom() * 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
});
document.getElementById('zoomOut')?.addEventListener('click', () => {
cy.zoom({
level: cy.zoom() / 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
});
document.getElementById('resetView')?.addEventListener('click', () => {
cy.fit();
resetSelection(cy);
});
// Legend-Toggle
document.getElementById('toggleLegend')?.addEventListener('click', () => {
const legend = document.getElementById('categoryLegend');
if (legend) {
isLegendVisible = !isLegendVisible;
legend.style.display = isLegendVisible ? 'block' : 'none';
}
});
// Keyboard-Controls
document.addEventListener('keydown', (e) => {
if (e.key === '+' || e.key === '=') {
cy.zoom({
level: cy.zoom() * 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
} else if (e.key === '-' || e.key === '_') {
cy.zoom({
level: cy.zoom() / 1.2,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
} else if (e.key === 'Escape') {
resetSelection(cy);
}
});
});
/**
* Aktualisiert das Info-Panel mit Knoteninformationen
* @param {Object} node - Der ausgewählte Knoten
*/
function updateInfoPanel(node) {
const infoPanel = document.getElementById('infoPanel');
if (!infoPanel) return;
const data = node.data();
const connectedNodes = node.neighborhood('node');
let html = `
<h3>${data.label || data.name}</h3>
<p class="category">${data.category || 'Keine Kategorie'}</p>
${data.description ? `<p class="description">${data.description}</p>` : ''}
<div class="connections">
<h4>Verbindungen (${connectedNodes.length})</h4>
<ul>
`;
connectedNodes.forEach(connectedNode => {
const connectedData = connectedNode.data();
html += `
<li style="color: ${connectedData.color || '#60a5fa'}">
${connectedData.label || connectedData.name}
</li>
`;
});
html += `
</ul>
</div>
`;
infoPanel.innerHTML = html;
infoPanel.style.display = 'block';
}
/**
* Aktualisiert die Seitenleiste mit Knoteninformationen
* @param {Object} node - Der ausgewählte Knoten
*/
function updateSidebar(node) {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const data = node.data();
const connectedNodes = node.neighborhood('node');
let html = `
<div class="node-details">
<h3>${data.label || data.name}</h3>
<p class="category">${data.category || 'Keine Kategorie'}</p>
${data.description ? `<p class="description">${data.description}</p>` : ''}
<div class="connections">
<h4>Verbindungen (${connectedNodes.length})</h4>
<ul>
`;
connectedNodes.forEach(connectedNode => {
const connectedData = connectedNode.data();
html += `
<li style="color: ${connectedData.color || '#60a5fa'}">
${connectedData.label || connectedData.name}
</li>
`;
});
html += `
</ul>
</div>
</div>
`;
sidebar.innerHTML = html;
}
/**
* Setzt die Auswahl zurück
* @param {Object} cy - Cytoscape-Instanz
*/
function resetSelection(cy) {
window.mindmapInstance.selectedNode = null;
// Alle Hervorhebungen zurücksetzen
cy.nodes().forEach(node => {
node.removeStyle();
node.connectedEdges().removeStyle();
});
// Info-Panel ausblenden
const infoPanel = document.getElementById('infoPanel');
if (infoPanel) {
infoPanel.style.display = 'none';
}
// Seitenleiste leeren
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.innerHTML = '';
}
}

View File

@@ -1,234 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interaktive Mindmap</title>
<!-- Cytoscape.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
<!-- Feather Icons (optional) -->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9fafb;
color: #111827;
line-height: 1.5;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.header {
background-color: #1f2937;
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 500;
}
.toolbar {
background-color: #f3f4f6;
padding: 0.75rem;
display: flex;
gap: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.btn {
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #6b7280;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-danger {
background-color: #ef4444;
}
.btn-danger:hover {
background-color: #dc2626;
}
.search-container {
flex: 1;
display: flex;
margin-left: 1rem;
}
.search-input {
width: 100%;
max-width: 300px;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.875rem;
}
#cy {
flex: 1;
width: 100%;
position: relative;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.category-filter {
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
color: white;
}
.category-filter:not(.active) {
opacity: 0.6;
}
.category-filter:hover:not(.active) {
opacity: 0.8;
}
.footer {
background-color: #f3f4f6;
padding: 0.75rem;
text-align: center;
font-size: 0.75rem;
color: #6b7280;
border-top: 1px solid #e5e7eb;
}
/* Kontextmenü Styling */
#context-menu {
position: absolute;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
#context-menu .menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
}
#context-menu .menu-item:hover {
background-color: #f3f4f6;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>Interaktive Mindmap</h1>
<div class="search-container">
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
</div>
</header>
<div class="toolbar">
<button id="addNode" class="btn">
<i data-feather="plus-circle"></i>
Knoten hinzufügen
</button>
<button id="addEdge" class="btn">
<i data-feather="git-branch"></i>
Verbindung erstellen
</button>
<button id="editNode" class="btn btn-secondary">
<i data-feather="edit-2"></i>
Knoten bearbeiten
</button>
<button id="deleteNode" class="btn btn-danger">
<i data-feather="trash-2"></i>
Knoten löschen
</button>
<button id="deleteEdge" class="btn btn-danger">
<i data-feather="scissors"></i>
Verbindung löschen
</button>
<button id="reLayout" class="btn btn-secondary">
<i data-feather="refresh-cw"></i>
Layout neu anordnen
</button>
<button id="exportMindmap" class="btn btn-secondary">
<i data-feather="download"></i>
Exportieren
</button>
</div>
<div id="category-filters" class="category-filters">
<!-- Wird dynamisch befüllt -->
</div>
<div id="cy"></div>
<footer class="footer">
Mindmap-Anwendung © 2023
</footer>
</div>
<!-- Unsere Mindmap JS -->
<script src="../js/mindmap.js"></script>
<!-- Icons initialisieren -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof feather !== 'undefined') {
feather.replace();
}
});
</script>
</body>
</html>

View File

@@ -1,659 +0,0 @@
/**
* Mindmap.js - Interaktive Mind-Map Implementierung
* - Cytoscape.js für Graph-Rendering
* - Fetch API für REST-Zugriffe
* - Socket.IO für Echtzeit-Synchronisation
*/
(async () => {
/* 1. Initialisierung und Grundkonfiguration */
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node',
style: {
'label': 'data(name)',
'text-valign': 'center',
'color': '#fff',
'background-color': 'data(color)',
'width': 45,
'height': 45,
'font-size': 11,
'text-outline-width': 1,
'text-outline-color': '#000',
'text-outline-opacity': 0.5,
'text-wrap': 'wrap',
'text-max-width': 80
}
},
{
selector: 'node[icon]',
style: {
'background-image': function(ele) {
return `static/img/icons/${ele.data('icon')}.svg`;
},
'background-width': '60%',
'background-height': '60%',
'background-position-x': '50%',
'background-position-y': '40%',
'text-margin-y': 10
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#888',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'target-arrow-color': '#888'
}
},
{
selector: ':selected',
style: {
'border-width': 3,
'border-color': '#f8f32b'
}
}
],
layout: {
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}
});
/* 2. Hilfs-Funktionen für API-Zugriffe */
const get = endpoint => fetch(endpoint).then(r => r.json());
const post = (endpoint, body) =>
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(r => r.json());
const del = endpoint =>
fetch(endpoint, { method: 'DELETE' }).then(r => r.json());
/* 3. Kategorien laden für Style-Informationen */
let categories = await get('/api/categories');
/* 4. Daten laden und Rendering */
const loadMindmap = async () => {
try {
// Nodes und Beziehungen parallel laden
const [nodes, relationships] = await Promise.all([
get('/api/mind_map_nodes'),
get('/api/node_relationships')
]);
// Graph leeren (für Reload-Fälle)
cy.elements().remove();
// Knoten zum Graph hinzufügen
cy.add(
nodes.map(node => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
return {
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
},
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
};
})
);
// Kanten zum Graph hinzufügen
cy.add(
relationships.map(rel => ({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
}))
);
// Layout anwenden wenn keine Positionsdaten vorhanden
const nodesWithoutPosition = cy.nodes().filter(node =>
!node.position() || (node.position().x === 0 && node.position().y === 0)
);
if (nodesWithoutPosition.length > 0) {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
}
// Tooltip-Funktionalität
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
const node = event.target;
const description = node.data('description');
if (description) {
const tooltip = document.getElementById('node-tooltip') ||
document.createElement('div');
if (!tooltip.id) {
tooltip.id = 'node-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = '#333';
tooltip.style.color = '#fff';
tooltip.style.padding = '8px';
tooltip.style.borderRadius = '4px';
tooltip.style.maxWidth = '250px';
tooltip.style.zIndex = 10;
tooltip.style.pointerEvents = 'none';
tooltip.style.transition = 'opacity 0.2s';
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
document.body.appendChild(tooltip);
}
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
tooltip.innerHTML = description;
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
tooltip.style.opacity = '1';
}
});
cy.nodes().unbind('mouseout').bind('mouseout', () => {
const tooltip = document.getElementById('node-tooltip');
if (tooltip) {
tooltip.style.opacity = '0';
}
});
} catch (error) {
console.error('Fehler beim Laden der Mindmap:', error);
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
}
};
// Initial laden
await loadMindmap();
/* 5. Socket.IO für Echtzeit-Synchronisation */
const socket = io();
socket.on('node_added', async (node) => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cy.add({
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
}
});
// Layout neu anwenden, wenn nötig
if (!node.x || !node.y) {
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
}
});
socket.on('node_updated', (node) => {
const cyNode = cy.$id(node.id.toString());
if (cyNode.length > 0) {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cyNode.data({
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
});
if (node.x && node.y) {
cyNode.position({ x: node.x, y: node.y });
}
}
});
socket.on('node_deleted', (nodeId) => {
const cyNode = cy.$id(nodeId.toString());
if (cyNode.length > 0) {
cy.remove(cyNode);
}
});
socket.on('relationship_added', (rel) => {
cy.add({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
});
});
socket.on('relationship_deleted', (rel) => {
const edgeId = `${rel.parent_id}_${rel.child_id}`;
const cyEdge = cy.$id(edgeId);
if (cyEdge.length > 0) {
cy.remove(cyEdge);
}
});
socket.on('category_updated', async () => {
// Kategorien neu laden
categories = await get('/api/categories');
// Nodes aktualisieren, die diese Kategorie verwenden
cy.nodes().forEach(node => {
const categoryId = node.data('category_id');
if (categoryId) {
const category = categories.find(c => c.id === categoryId);
if (category) {
node.data('color', node.data('color_code') || category.color_code);
node.data('icon', node.data('icon') || category.icon);
}
}
});
});
/* 6. UI-Interaktionen */
// Knoten hinzufügen
const btnAddNode = document.getElementById('addNode');
if (btnAddNode) {
btnAddNode.addEventListener('click', async () => {
const name = prompt('Knotenname eingeben:');
if (!name) return;
const description = prompt('Beschreibung (optional):');
// Kategorie auswählen
let categoryId = null;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
'0'
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten erstellen
await post('/api/mind_map_node', {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Verbindung hinzufügen
const btnAddEdge = document.getElementById('addEdge');
if (btnAddEdge) {
btnAddEdge.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 2) {
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
return;
}
const [parent, child] = sel.map(node => node.id());
await post('/api/node_relationship', {
parent_id: parent,
child_id: child
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten bearbeiten
const btnEditNode = document.getElementById('editNode');
if (btnEditNode) {
btnEditNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
const node = sel[0];
const nodeData = node.data();
const name = prompt('Knotenname:', nodeData.name);
if (!name) return;
const description = prompt('Beschreibung:', nodeData.description || '');
// Kategorie auswählen
let categoryId = nodeData.category_id;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
categories.findIndex(c => c.id === categoryId).toString()
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten aktualisieren
await post(`/api/mind_map_node/${nodeData.id}`, {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten löschen
const btnDeleteNode = document.getElementById('deleteNode');
if (btnDeleteNode) {
btnDeleteNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
const nodeId = sel[0].id();
await del(`/api/mind_map_node/${nodeId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Verbindung löschen
const btnDeleteEdge = document.getElementById('deleteEdge');
if (btnDeleteEdge) {
btnDeleteEdge.addEventListener('click', async () => {
const sel = cy.$('edge:selected');
if (sel.length !== 1) {
alert('Bitte genau eine Verbindung auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
const edge = sel[0];
const parentId = edge.source().id();
const childId = edge.target().id();
await del(`/api/node_relationship/${parentId}/${childId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Layout aktualisieren
const btnReLayout = document.getElementById('reLayout');
if (btnReLayout) {
btnReLayout.addEventListener('click', () => {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
});
}
/* 7. Position speichern bei Drag & Drop */
cy.on('dragfree', 'node', async (e) => {
const node = e.target;
const position = node.position();
await post(`/api/mind_map_node/${node.id()}/position`, {
x: Math.round(position.x),
y: Math.round(position.y)
});
// Andere Benutzer erhalten die Position über den node_updated Event
});
/* 8. Kontextmenü (optional) */
const setupContextMenu = () => {
cy.on('cxttap', 'node', function(e) {
const node = e.target;
const nodeData = node.data();
// Position des Kontextmenüs berechnen
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
const menuX = containerRect.left + renderedPosition.x;
const menuY = containerRect.top + renderedPosition.y;
// Kontextmenü erstellen oder aktualisieren
let contextMenu = document.getElementById('context-menu');
if (!contextMenu) {
contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.backgroundColor = '#fff';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.borderRadius = '4px';
contextMenu.style.padding = '5px 0';
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
contextMenu.style.zIndex = 1000;
document.body.appendChild(contextMenu);
}
// Menüinhalte
contextMenu.innerHTML = `
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
<div class="menu-item" data-action="delete">Knoten löschen</div>
`;
// Styling für Menüpunkte
const menuItems = contextMenu.querySelectorAll('.menu-item');
menuItems.forEach(item => {
item.style.padding = '8px 20px';
item.style.cursor = 'pointer';
item.style.fontSize = '14px';
item.addEventListener('mouseover', function() {
this.style.backgroundColor = '#f0f0f0';
});
item.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
// Event-Handler
item.addEventListener('click', async function() {
const action = this.getAttribute('data-action');
switch(action) {
case 'edit':
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
const name = prompt('Knotenname:', nodeData.name);
if (name) {
const description = prompt('Beschreibung:', nodeData.description || '');
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
}
break;
case 'connect':
// Modus zum Verbinden aktivieren
cy.nodes().unselect();
node.select();
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
break;
case 'delete':
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
await del(`/api/mind_map_node/${nodeData.id}`);
}
break;
}
// Menü schließen
contextMenu.style.display = 'none';
});
});
// Menü positionieren und anzeigen
contextMenu.style.left = menuX + 'px';
contextMenu.style.top = menuY + 'px';
contextMenu.style.display = 'block';
// Event-Listener zum Schließen des Menüs
const closeMenu = function() {
if (contextMenu) {
contextMenu.style.display = 'none';
}
document.removeEventListener('click', closeMenu);
};
// Verzögerung, um den aktuellen Click nicht zu erfassen
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
e.preventDefault();
});
};
// Kontextmenü aktivieren (optional)
// setupContextMenu();
/* 9. Export-Funktion (optional) */
const btnExport = document.getElementById('exportMindmap');
if (btnExport) {
btnExport.addEventListener('click', () => {
const elements = cy.json().elements;
const exportData = {
nodes: elements.nodes.map(n => ({
id: n.data.id,
name: n.data.name,
description: n.data.description,
category_id: n.data.category_id,
x: Math.round(n.position?.x || 0),
y: Math.round(n.position?.y || 0)
})),
relationships: elements.edges.map(e => ({
parent_id: e.data.source,
child_id: e.data.target
}))
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mindmap_export.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
/* 10. Filter-Funktion nach Kategorien (optional) */
const setupCategoryFilters = () => {
const filterContainer = document.getElementById('category-filters');
if (!filterContainer || !categories.length) return;
filterContainer.innerHTML = '';
// "Alle anzeigen" Option
const allBtn = document.createElement('button');
allBtn.innerText = 'Alle Kategorien';
allBtn.className = 'category-filter active';
allBtn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
allBtn.classList.add('active');
cy.nodes().removeClass('filtered').show();
cy.edges().show();
};
filterContainer.appendChild(allBtn);
// Filter-Button pro Kategorie
categories.forEach(category => {
const btn = document.createElement('button');
btn.innerText = category.name;
btn.className = 'category-filter';
btn.style.backgroundColor = category.color_code;
btn.style.color = '#fff';
btn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
btn.classList.add('active');
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
cy.nodes().addClass('filtered').hide();
matchingNodes.removeClass('filtered').show();
// Verbindungen zu/von diesen Knoten anzeigen
cy.edges().hide();
matchingNodes.connectedEdges().show();
};
filterContainer.appendChild(btn);
});
};
// Filter-Funktionalität aktivieren (optional)
// setupCategoryFilters();
/* 11. Suchfunktion (optional) */
const searchInput = document.getElementById('search-mindmap');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
if (!searchTerm) {
cy.nodes().removeClass('search-hidden').show();
cy.edges().show();
return;
}
cy.nodes().forEach(node => {
const name = node.data('name').toLowerCase();
const description = (node.data('description') || '').toLowerCase();
if (name.includes(searchTerm) || description.includes(searchTerm)) {
node.removeClass('search-hidden').show();
node.connectedEdges().show();
} else {
node.addClass('search-hidden').hide();
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
node.connectedEdges().forEach(edge => {
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
if (otherNode.hasClass('search-hidden')) {
edge.hide();
}
});
}
});
});
}
console.log('Mindmap erfolgreich initialisiert');
})();

View File

@@ -247,130 +247,63 @@ class ChatGPTAssistant {
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%]';
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
let formattedText = '';
if (sender === 'assistant' && this.markdownParser) {
// Für Assistentnachrichten Markdown verwenden
try {
formattedText = this.markdownParser.parse(text);
// CSS für Markdown-Formatierung hinzufügen
const markdownStyles = `
.markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3,
.markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 {
font-weight: bold;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.markdown-bubble h1 { font-size: 1.4rem; }
.markdown-bubble h2 { font-size: 1.3rem; }
.markdown-bubble h3 { font-size: 1.2rem; }
.markdown-bubble h4 { font-size: 1.1rem; }
.markdown-bubble ul, .markdown-bubble ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.markdown-bubble ul { list-style-type: disc; }
.markdown-bubble ol { list-style-type: decimal; }
.markdown-bubble p { margin: 0.5rem 0; }
.markdown-bubble code {
font-family: monospace;
background-color: rgba(0, 0, 0, 0.1);
padding: 1px 4px;
border-radius: 3px;
}
.markdown-bubble pre {
background-color: rgba(0, 0, 0, 0.1);
padding: 0.5rem;
border-radius: 4px;
overflow-x: auto;
margin: 0.5rem 0;
}
.markdown-bubble pre code {
background-color: transparent;
padding: 0;
}
.markdown-bubble blockquote {
border-left: 3px solid rgba(0, 0, 0, 0.2);
padding-left: 0.8rem;
margin: 0.5rem 0;
font-style: italic;
}
.dark .markdown-bubble code {
background-color: rgba(255, 255, 255, 0.1);
}
.dark .markdown-bubble pre {
background-color: rgba(255, 255, 255, 0.1);
}
.dark .markdown-bubble blockquote {
border-left-color: rgba(255, 255, 255, 0.2);
}
`;
// Füge die Styles hinzu, wenn sie noch nicht vorhanden sind
if (!document.querySelector('#markdown-chat-styles')) {
const style = document.createElement('style');
style.id = 'markdown-chat-styles';
style.textContent = markdownStyles;
document.head.appendChild(style);
}
// Klasse für Markdown-Formatierung hinzufügen
bubble.classList.add('markdown-bubble');
} catch (error) {
console.error('Fehler bei der Markdown-Formatierung:', error);
// Fallback zur einfachen Formatierung
formattedText = text.split('\n').map(line => {
if (line.trim() === '') return '<br>';
return `<p>${line}</p>`;
}).join('');
}
? 'user-message rounded-lg py-2 px-3 max-w-[85%]'
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
if (this.markdownParser) {
bubble.innerHTML = this.markdownParser.parse(text);
} else {
// Für Benutzernachrichten einfache Formatierung
formattedText = text.split('\n').map(line => {
if (line.trim() === '') return '<br>';
return `<p>${line}</p>`;
}).join('');
bubble.textContent = text;
}
bubble.innerHTML = formattedText;
// Links in der Nachricht klickbar machen
const links = bubble.querySelectorAll('a');
links.forEach(link => {
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'text-primary-600 dark:text-primary-400 underline';
});
// Code-Blöcke stylen
const codeBlocks = bubble.querySelectorAll('pre');
codeBlocks.forEach(block => {
block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto';
});
const inlineCode = bubble.querySelectorAll('code:not(pre code)');
inlineCode.forEach(code => {
code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm';
});
messageEl.appendChild(bubble);
this.chatHistory.appendChild(messageEl);
if (this.chatHistory) {
this.chatHistory.appendChild(messageEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
// Scrolle zum Ende des Chat-Verlaufs
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
/**
* Zeigt Vorschläge als klickbare Pills an
* @param {string[]} suggestions - Liste von Vorschlägen
* Zeigt Vorschläge für mögliche Fragen an
* @param {Array} suggestions - Array von Vorschlägen
*/
showSuggestions(suggestions) {
if (!this.suggestionArea) return;
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
// Vorherige Vorschläge entfernen
this.suggestionArea.innerHTML = '';
if (suggestions && suggestions.length > 0) {
suggestions.forEach(suggestion => {
const pill = document.createElement('button');
pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors';
pill.textContent = suggestion;
this.suggestionArea.appendChild(pill);
});
this.suggestionArea.classList.remove('hidden');
} else {
this.suggestionArea.classList.add('hidden');
}
// Neue Vorschläge hinzufügen
suggestions.forEach((text, index) => {
const pill = document.createElement('button');
pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200';
pill.style.animationDelay = `${index * 0.1}s`;
pill.textContent = text;
this.suggestionArea.appendChild(pill);
});
// Vorschlagsbereich anzeigen
this.suggestionArea.classList.remove('hidden');
}
/**
@@ -409,14 +342,27 @@ class ChatGPTAssistant {
messages: this.messages
}),
cache: 'no-cache', // Kein Cache verwenden
credentials: 'same-origin' // Cookies senden
credentials: 'same-origin', // Cookies senden
timeout: 60000 // 60 Sekunden Timeout
});
// Ladeindikator entfernen
this.removeLoadingIndicator();
if (!response.ok) {
throw new Error(`Serverfehler: ${response.status} ${response.statusText}`);
const errorText = await response.text();
let errorMessage;
try {
// Versuche, die Fehlermeldung zu parsen
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`;
} catch {
// Bei Parsing-Fehler verwende Standardfehlermeldung
errorMessage = `Serverfehler: ${response.status} ${response.statusText}`;
}
throw new Error(errorMessage);
}
const data = await response.json();
@@ -442,24 +388,45 @@ class ChatGPTAssistant {
// Ladeindikator entfernen, falls noch vorhanden
this.removeLoadingIndicator();
// Spezielle Fehlermeldungen für bestimmte Fehlertypen
const errorMessage = error.message || '';
let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.';
if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) {
userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.';
} else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) {
userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.';
} else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.';
}
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
if (this.retryCount < this.maxRetries) {
this.retryCount++;
this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...');
this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`);
// Kurze Verzögerung vor dem erneuten Versuch
setTimeout(() => {
// Letzte Benutzernachricht aus dem Messages-Array entfernen
const lastUserMessage = this.messages[this.messages.length - 2].content;
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
if (lastUserMessageIndex >= 0) {
const lastUserMessage = this.messages[lastUserMessageIndex].content;
// Erneuter Versand mit gleicher Nachricht
this.inputField.value = lastUserMessage;
this.sendMessage();
}, 1500);
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
setTimeout(() => {
// Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht
this.messages = this.messages.filter(msg =>
!(msg.role === 'assistant' && msg.content.includes('versuche es erneut'))
);
// Erneuter Versand mit gleicher Nachricht
this.inputField.value = lastUserMessage;
this.sendMessage();
}, retryDelay);
}
} else {
// Maximale Anzahl an Wiederholungsversuchen erreicht
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.');
this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.');
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
}
} finally {
@@ -512,26 +479,33 @@ class ChatGPTAssistant {
}
/**
* Zeigt einen Ladeindikator im Chat an
* Zeigt eine Ladeanimation an
*/
showLoadingIndicator() {
if (!this.chatHistory) return;
// Entferne vorhandenen Ladeindikator (falls vorhanden)
this.removeLoadingIndicator();
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
if (document.getElementById('assistant-loading-indicator')) return;
const loadingEl = document.createElement('div');
loadingEl.id = 'assistant-loading';
loadingEl.className = 'flex justify-start';
loadingEl.id = 'assistant-loading-indicator';
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>';
bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
const typingIndicator = document.createElement('div');
typingIndicator.className = 'typing-indicator';
typingIndicator.innerHTML = `
<span></span>
<span></span>
<span></span>
`;
bubble.appendChild(typingIndicator);
loadingEl.appendChild(bubble);
this.chatHistory.appendChild(loadingEl);
// Scroll zum Ende des Verlaufs
this.chatHistory.appendChild(loadingEl);
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
}
@@ -539,7 +513,7 @@ class ChatGPTAssistant {
* Entfernt den Ladeindikator aus dem Chat
*/
removeLoadingIndicator() {
const loadingIndicator = document.getElementById('assistant-loading');
const loadingIndicator = document.getElementById('assistant-loading-indicator');
if (loadingIndicator) {
loadingIndicator.remove();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,844 +0,0 @@
/**
* 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
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}, versuche erneute Verbindung`);
// Bei Verbindungsfehler versuchen, die Verbindung neu herzustellen
const retryResponse = await fetch('/api/refresh-mindmap', {
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
});
if (!retryResponse.ok) {
throw new Error(`Retry failed with status: ${retryResponse.status}`);
}
const retryData = await retryResponse.json();
if (!retryData.success || !retryData.nodes || retryData.nodes.length === 0) {
console.warn('Keine Mindmap-Daten nach Neuversuch, verwende weiterhin Standard-Daten.');
return; // Keep using default data
}
// Flache Liste von Knoten und Verbindungen erstellen
this.nodes = [];
this.links = [];
// Knoten direkt übernehmen
retryData.nodes.forEach(node => {
this.nodes.push({
id: node.id,
name: node.name,
description: node.description || '',
thought_count: node.thought_count || 0,
color: this.generateColorFromString(node.name),
});
// Verbindungen hinzufügen
if (node.connections && node.connections.length > 0) {
node.connections.forEach(conn => {
this.links.push({
source: node.id,
target: conn.target
});
});
}
});
// Visualisierung aktualisieren mit den tatsächlichen Daten
this.updateVisualization();
return;
}
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);
});
}
/**
* Gibt alle direkt verbundenen Knoten eines Knotens zurück
* @param {Object} node - Der Knoten, für den die Verbindungen gesucht werden
* @returns {Array} Array der verbundenen Knotenobjekte
*/
getConnectedNodes(node) {
if (!node || !this.links || !this.nodes) return [];
const nodeId = node.id;
const connectedIds = new Set();
this.links.forEach(link => {
if (link.source === nodeId || (link.source && link.source.id === nodeId)) {
connectedIds.add(link.target.id ? link.target.id : link.target);
}
if (link.target === nodeId || (link.target && link.target.id === nodeId)) {
connectedIds.add(link.source.id ? link.source.id : link.source);
}
});
return this.nodes.filter(n => connectedIds.has(n.id));
}
}
// Exportiere die Klasse für die Verwendung in anderen Modulen
window.MindMapVisualization = MindMapVisualization;

1133
static/js/social.js Normal file

File diff suppressed because it is too large Load Diff

2724
static/js/update_mindmap.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,11 @@ class NeuralNetworkBackground {
// Standardkonfiguration mit subtileren Werten
this.config = {
nodeCount: 50, // Weniger Knoten
nodeCount: 10, // Weniger Knoten
nodeSize: 1.2, // Kleinere Knoten
connectionDistance: 150, // Reduzierte Verbindungsdistanz
connectionOpacity: 0.3, // Sanftere Verbindungslinien
clusterCount: 2, // Weniger Cluster
clusterCount: 7, // Weniger Cluster
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
animationSpeed: 0.25, // Langsamere Animation
flowDensity: 0.05, // Deutlich weniger Flussanimationen
@@ -1106,4 +1106,66 @@ window.addEventListener('beforeunload', () => {
if (window.neuralNetworkBackground) {
window.neuralNetworkBackground.destroy();
}
});
});
function applyNeuralNetworkStyle(cy) {
cy.style()
.selector('node')
.style({
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'color': 'data(fontColor)',
'text-outline-width': 2,
'text-outline-color': 'rgba(0,0,0,0.8)',
'text-outline-opacity': 0.9,
'font-size': 'data(fontSize)',
'font-weight': '500',
'text-margin-y': 8,
'width': function(ele) {
if (ele.data('isCenter')) return 120;
return ele.data('neuronSize') ? ele.data('neuronSize') * 10 : 80;
},
'height': function(ele) {
if (ele.data('isCenter')) return 120;
return ele.data('neuronSize') ? ele.data('neuronSize') * 10 : 80;
},
'background-color': 'data(color)',
'background-opacity': 0.9,
'border-width': 2,
'border-color': '#ffffff',
'border-opacity': 0.8,
'shape': 'ellipse',
'transition-property': 'background-color, background-opacity, border-width',
'transition-duration': '0.3s',
'transition-timing-function': 'ease-in-out'
})
.selector('edge')
.style({
'width': function(ele) {
return ele.data('strength') ? ele.data('strength') * 3 : 1;
},
'curve-style': 'bezier',
'line-color': function(ele) {
const sourceColor = ele.source().data('color');
return sourceColor || '#8a8aaa';
},
'line-opacity': function(ele) {
return ele.data('strength') ? ele.data('strength') * 0.8 : 0.4;
},
'line-style': function(ele) {
const strength = ele.data('strength');
if (!strength) return 'solid';
if (strength <= 0.4) return 'dotted';
if (strength <= 0.6) return 'dashed';
return 'solid';
},
'target-arrow-shape': 'none',
'source-endpoint': '0% 50%',
'target-endpoint': '100% 50%',
'transition-property': 'line-opacity, width',
'transition-duration': '0.3s',
'transition-timing-function': 'ease-in-out'
})
.update();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Datenbank aktualisieren{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-10">
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
<h1 class="text-2xl font-bold text-purple-400 mb-4">Datenbank aktualisieren</h1>
{% if message %}
<div class="mb-6 p-4 rounded-lg {{ 'bg-green-800 bg-opacity-50' if success else 'bg-red-800 bg-opacity-50' }}">
<p class="text-white">{{ message }}</p>
</div>
{% endif %}
<div class="mb-6">
<p class="text-gray-300 mb-4">
Diese Funktion aktualisiert die Datenbankstruktur, um mit dem aktuellen Datenmodell kompatibel zu sein.
Dabei werden folgende Änderungen vorgenommen:
</p>
<ul class="list-disc pl-6 text-gray-300 mb-6">
<li>Hinzufügen von <code>bio</code>, <code>location</code>, <code>website</code>, <code>avatar</code> und <code>last_login</code> zur Benutzer-Tabelle</li>
</ul>
<div class="bg-yellow-800 bg-opacity-30 p-4 rounded-lg mb-6">
<p class="text-yellow-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>Warnung:</strong> Bitte stelle sicher, dass du ein Backup der Datenbank erstellt hast, bevor du fortfährst.
</p>
</div>
</div>
<form method="POST" action="{{ url_for('admin_update_database') }}">
<div class="flex justify-between">
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
Zurück zur Startseite
</a>
<button type="submit" class="px-4 py-2 bg-purple-700 text-white rounded-lg hover:bg-purple-600">
Datenbank aktualisieren
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -6,8 +6,7 @@
<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">
<link rel="icon" href="{{ url_for('static', filename='img/neuron-favicon.svg') }}" type="image/svg+xml">
<!-- Meta Tags -->
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
@@ -59,6 +58,20 @@
800: '#0e1220',
900: '#0a0e19'
}
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-5px)' }
},
'bounce-slow': {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-8px)' }
}
},
animation: {
float: 'float 3s ease-in-out infinite',
'bounce-slow': 'bounce-slow 2s ease-in-out infinite'
}
}
}
@@ -69,8 +82,8 @@
<link href="{{ url_for('static', filename='fonts/inter.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='fonts/jetbrains-mono.css') }}" rel="stylesheet">
<!-- Icons - Self-hosted Font Awesome -->
<link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet">
<!-- Font Awesome vom CDN -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
<!-- Assistent CSS -->
<link href="{{ url_for('static', filename='css/assistant.css') }}" rel="stylesheet">
@@ -87,6 +100,9 @@
<!-- Neural Network Background CSS -->
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
<!-- Mindmap CSS -->
<link href="{{ url_for('static', filename='css/mindmap.css', v='1.0.1') }}" rel="stylesheet">
<!-- D3.js für Visualisierungen -->
<script src="https://d3js.org/d3.v7.min.js"></script>
@@ -95,12 +111,6 @@
<!-- ChatGPT Assistant -->
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
<!-- MindMap Visualization Module -->
<script src="{{ url_for('static', filename='js/modules/mindmap.js') }}"></script>
<!-- MindMap Page Module -->
<script src="{{ url_for('static', filename='js/modules/mindmap-page.js') }}"></script>
<!-- Neural Network Background Script -->
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
@@ -111,18 +121,20 @@
<!-- Seitenspezifische Styles -->
{% block extra_css %}{% endblock %}
<!-- Custom dark mode styles -->
<!-- Custom dark/light mode styles -->
<!-- ► ► FarbToken strikt getrennt ◄ ◄ -->
<style>
/* LightMode */
:root {
--bg-primary:#f4f6fa;
--bg-secondary:#e9ecf3;
--bg-primary:#f8fafc;
--bg-secondary:#f1f5f9;
--text-primary:#232837;
--text-secondary:#475569;
--accent-primary:#7c3aed;
--accent-secondary:#8b5cf6;
--glow-effect:0 0 8px rgba(139,92,246,.08);
background-image: linear-gradient(to bottom right, rgba(248, 250, 252, 0.8), rgba(241, 245, 249, 0.8));
background-attachment: fixed;
}
/* DarkMode */
.dark {
@@ -136,7 +148,8 @@
}
body {
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)] transition-colors duration-300;
@apply min-h-screen bg-[color:var(--bg-primary)] text-[color:var(--text-primary)];
transition: background-color 0.5s ease-in-out, color 0.3s ease-in-out, background-image 0.5s ease-in-out;
}
/* Utilities */
@@ -149,7 +162,152 @@
.glass-navbar { @apply glass-morphism border backdrop-blur-xl; }
.light .glass-navbar { background-color:rgba(255,255,255,.8); border-color:rgba(0,0,0,.05); }
.dark .glass-navbar { background-color:rgba(10,14,25,.8); border-color:rgba(255,255,255,.05); }
</style>
/* Light-Mode spezifische Stile */
body:not(.dark) {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.nav-link-light {
color: var(--text-secondary);
transition: all 0.3s ease;
}
.nav-link-light:hover {
color: var(--text-primary);
background-color: rgba(126, 34, 206, 0.1);
}
.nav-link-light-active {
color: var(--accent-primary);
background-color: rgba(126, 34, 206, 0.15);
font-weight: 500;
}
/* Kartendesign im Light-Mode */
body:not(.dark) .card {
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
body:not(.dark) .card:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
a:hover {
color: var(--light-primary-hover);
}
/* Light Mode Buttons */
body:not(.dark) .btn,
body:not(.dark) button:not(.toggle) {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
color: white !important;
border: none;
box-shadow: 0 2px 4px rgba(124, 58, 237, 0.25);
border-radius: 8px;
padding: 0.625rem 1.25rem;
transition: all 0.2s ease;
font-weight: 600;
letter-spacing: 0.02em;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
body:not(.dark) .btn:hover,
body:not(.dark) button:not(.toggle):hover {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
color: white !important;
}
/* KI-Chat Button im Light-Mode */
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"] {
background: linear-gradient(135deg, #7c3aed, #4f46e5);
color: white !important;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
body:not(.dark) [onclick*="MindMap.assistant.toggleAssistant"]:hover {
background: linear-gradient(135deg, #8b5cf6, #6366f1);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
/* Style improvements for the theme toggle button */
.theme-toggle {
position: relative;
width: 48px;
height: 24px;
border-radius: 24px;
padding: 2px;
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
}
body.dark .theme-toggle {
background: linear-gradient(to right, #7c3aed, #3b82f6);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 10px rgba(124, 58, 237, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
body:not(.dark) .theme-toggle {
background: linear-gradient(to right, #8b5cf6, #60a5fa);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 10px rgba(124, 58, 237, 0.15);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.theme-toggle::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
top: 2px;
transition: all 0.3s ease;
z-index: 2;
}
body.dark .theme-toggle::after {
background: #f1f5f9 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' width='14' height='14'%3E%3Cpath d='M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
transform: translateX(24px);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
body:not(.dark) .theme-toggle::after {
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b' width='14' height='14'%3E%3Cpath d='M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z'%3E%3C/path%3E%3C/svg%3E") no-repeat center center;
transform: translateX(2px);
box-shadow: 0 0 8px rgba(124, 58, 237, 0.2);
}
.theme-toggle:hover::after {
box-shadow: 0 0 12px rgba(124, 58, 237, 0.4);
}
/* Fixes for light mode button text colors */
body:not(.dark) .btn-primary {
color: white !important;
}
/* Fix for KI-Chat container */
#chatgpt-assistant {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 100;
}
.chat-assistant {
max-height: 80vh !important;
}
.chat-assistant .chat-messages {
max-height: calc(80vh - 160px) !important;
}
</style>
</head>
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
darkMode: true,
@@ -158,6 +316,17 @@
showSettingsModal: false,
init() {
this.initDarkMode();
},
initDarkMode() {
// Lade zuerst den Wert aus dem localStorage (client-seitig)
const storedMode = localStorage.getItem('colorMode');
if (storedMode) {
this.darkMode = storedMode === 'dark';
}
// Dann hole die Server-Einstellung, die Vorrang hat
this.fetchDarkModeFromSession();
},
@@ -167,7 +336,7 @@
.then(data => {
if (data.success) {
this.darkMode = data.darkMode === 'true';
document.querySelector('html').classList.toggle('dark', this.darkMode);
this.applyDarkMode();
}
})
.catch(error => {
@@ -175,10 +344,17 @@
});
},
applyDarkMode() {
document.querySelector('html').classList.toggle('dark', this.darkMode);
document.querySelector('body').classList.toggle('dark', this.darkMode);
localStorage.setItem('colorMode', this.darkMode ? 'dark' : 'light');
},
toggleDarkMode() {
this.darkMode = !this.darkMode;
document.querySelector('html').classList.toggle('dark', this.darkMode);
this.applyDarkMode();
// Server über Änderung informieren
fetch('/api/set_dark_mode', {
method: 'POST',
headers: {
@@ -189,12 +365,10 @@
.then(response => response.json())
.then(data => {
if (data.success) {
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 => {
@@ -210,6 +384,7 @@
<div class="container mx-auto flex justify-between items-center">
<!-- Logo -->
<a href="{{ url_for('index') }}" class="flex items-center group">
<img src="{{ url_for('static', filename='img/neuron-logo.svg') }}" alt="Systades Logo" class="w-8 h-8 mr-2 transform transition-transform group-hover:scale-110">
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
</a>
@@ -229,6 +404,22 @@
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('social_feed') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
? '{{ 'nav-link-active' if request.endpoint == 'social_feed' else '' }}'
: '{{ 'nav-link-light-active' if request.endpoint == 'social_feed' else 'nav-link-light' }}'">
<i class="fa-solid fa-home mr-2"></i>Feed
</a>
<a href="{{ url_for('discover') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
? '{{ 'nav-link-active' if request.endpoint == 'discover' else '' }}'
: '{{ 'nav-link-light-active' if request.endpoint == 'discover' else 'nav-link-light' }}'">
<i class="fa-solid fa-compass mr-2"></i>Entdecken
</a>
{% endif %}
<a href="{{ url_for('search_thoughts_page') }}"
class="nav-link flex items-center"
x-bind:class="darkMode
@@ -241,7 +432,7 @@
class="nav-link flex items-center"
x-bind:class="darkMode
? 'bg-gradient-to-r from-purple-900/90 to-indigo-800/90 text-white font-medium px-4 py-2 rounded-xl hover:shadow-lg hover:shadow-purple-800/30 transition-all duration-300'
: 'bg-gradient-to-r from-purple-600/30 to-indigo-500/30 text-gray-800 font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
: 'bg-gradient-to-r from-purple-600 to-indigo-500 text-white font-medium px-4 py-2 rounded-xl hover:shadow-md transition-all duration-300'">
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
</button>
{% if current_user.is_authenticated %}
@@ -257,25 +448,14 @@
<!-- 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-purple-800/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-purple-600 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>
<!-- Dark/Light Mode Schalter -->
<button
@click="toggleDarkMode()"
class="theme-toggle relative w-12 h-6 rounded-full transition-all duration-300 flex items-center overflow-hidden"
aria-label="Dark Mode umschalten"
>
<span class="sr-only" x-text="darkMode ? 'Zum Light Mode wechseln' : 'Zum Dark Mode wechseln'"></span>
</button>
<!-- Profil-Link oder Login -->
{% if current_user.is_authenticated %}
<div class="relative" x-data="{ open: false }">
@@ -289,12 +469,21 @@
{% 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() }}
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#user-gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="user-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{% 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>
<span class="hidden md:block">{{ current_user.username }}</span>
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
</button>
<!-- Dropdown-Menü -->
@@ -400,6 +589,22 @@
: '{{ '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>
{% if current_user.is_authenticated %}
<a href="{{ url_for('social_feed') }}"
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 == 'social_feed' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'social_feed' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-home w-5 mr-3"></i>Feed
</a>
<a href="{{ url_for('discover') }}"
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 == 'discover' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'discover' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
<i class="fa-solid fa-compass w-5 mr-3"></i>Entdecken
</a>
{% endif %}
<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
@@ -412,7 +617,7 @@
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'">
: 'bg-gradient-to-r from-purple-600 to-blue-500 text-white hover:from-purple-600/90 hover:to-blue-500/90'">
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
</button>
{% if current_user.is_authenticated %}
@@ -478,6 +683,10 @@
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Mindmap
</a>
<a href="{{ url_for('search_thoughts_page') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Suche
</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'">
@@ -506,6 +715,10 @@
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Impressum
</a>
<a href="{{ url_for('ueber_uns') }}" class="text-sm transition-all duration-200"
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
Über uns
</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
@@ -553,58 +766,286 @@
<!-- Hilfsscripts -->
{% block scripts %}{% endblock %}
{% block extra_js %}{% endblock %}
<!-- KI-Chat Initialisierung -->
<!-- ChatGPT Initialisierung -->
<script>
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
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 = {};
// Prüfe, ob ChatGPTAssistant bereits existiert
if (typeof ChatGPTAssistant === 'undefined') {
class ChatGPTAssistant {
constructor() {
this.chatContainer = null;
this.messages = [];
this.isOpen = false;
}
init() {
// Chat-Container erstellen, falls noch nicht vorhanden
if (!document.getElementById('chat-assistant-container')) {
this.createChatInterface();
}
// Event-Listener für Chat-Button
const chatButton = document.getElementById('chat-assistant-button');
if (chatButton) {
chatButton.addEventListener('click', () => this.toggleChat());
}
// Event-Listener für Senden-Button
const sendButton = document.getElementById('chat-send-button');
if (sendButton) {
sendButton.addEventListener('click', () => this.sendMessage());
}
// Event-Listener für Eingabefeld (Enter-Taste)
const inputField = document.getElementById('chat-input');
if (inputField) {
inputField.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.sendMessage();
}
});
}
console.log('KI-Assistent erfolgreich initialisiert');
}
createChatInterface() {
// Chat-Button erstellen
const chatButton = document.createElement('button');
chatButton.id = 'chat-assistant-button';
chatButton.className = 'fixed bottom-6 right-6 bg-primary-600 text-white rounded-full p-4 shadow-lg z-50 hover:bg-primary-700 transition-all';
chatButton.innerHTML = '<i class="fas fa-robot text-xl"></i>';
document.body.appendChild(chatButton);
// Chat-Container erstellen
const chatContainer = document.createElement('div');
chatContainer.id = 'chat-assistant-container';
chatContainer.className = 'fixed bottom-24 right-6 w-80 md:w-96 bg-white dark:bg-gray-800 rounded-xl shadow-xl z-50 flex flex-col transition-all duration-300 transform scale-0 origin-bottom-right';
chatContainer.style.height = '500px';
chatContainer.style.maxHeight = '70vh';
// Chat-Header
chatContainer.innerHTML = `
<div class="p-4 border-b dark:border-gray-700 flex justify-between items-center">
<h3 class="font-bold text-gray-800 dark:text-white">Systades Assistent</h3>
<button id="chat-close-button" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
</div>
<div id="chat-messages" class="flex-1 overflow-y-auto p-4 space-y-4"></div>
<div class="p-4 border-t dark:border-gray-700">
<div class="flex space-x-2">
<input id="chat-input" type="text" placeholder="Frage stellen..." class="flex-1 px-4 py-2 rounded-lg border dark:border-gray-700 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500">
<button id="chat-send-button" class="bg-primary-600 text-white px-4 py-2 rounded-lg hover:bg-primary-700 transition-all">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
`;
document.body.appendChild(chatContainer);
this.chatContainer = chatContainer;
// Event-Listener für Schließen-Button
const closeButton = document.getElementById('chat-close-button');
if (closeButton) {
closeButton.addEventListener('click', () => this.toggleChat());
}
}
toggleChat() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.chatContainer.classList.remove('scale-0');
this.chatContainer.classList.add('scale-100');
} else {
this.chatContainer.classList.remove('scale-100');
this.chatContainer.classList.add('scale-0');
}
}
async sendMessage() {
const inputField = document.getElementById('chat-input');
const messageText = inputField.value.trim();
if (!messageText) return;
// Benutzer-Nachricht anzeigen
this.addMessage('user', messageText);
inputField.value = '';
// Lade-Indikator anzeigen
this.addMessage('assistant', '...', 'loading-message');
try {
// API-Anfrage senden
const response = await fetch('/api/assistant', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
messages: this.messages.map(msg => ({
role: msg.role,
content: msg.content
}))
})
});
const data = await response.json();
// Lade-Nachricht entfernen
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.remove();
}
if (data.error) {
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten: ' + data.error);
} else {
this.addMessage('assistant', data.response);
}
} catch (error) {
console.error('Fehler bei der API-Anfrage:', error);
// Lade-Nachricht entfernen
const loadingMessage = document.getElementById('loading-message');
if (loadingMessage) {
loadingMessage.remove();
}
this.addMessage('assistant', 'Entschuldigung, es ist ein Fehler aufgetreten. Bitte versuche es später erneut.');
}
}
addMessage(role, content, id = null) {
const messagesContainer = document.getElementById('chat-messages');
// Nachricht zum Array hinzufügen (außer Lade-Nachrichten)
if (id !== 'loading-message') {
this.messages.push({ role, content });
}
// Nachricht zum DOM hinzufügen
const messageElement = document.createElement('div');
messageElement.className = `p-3 rounded-lg ${role === 'user' ? 'bg-primary-100 dark:bg-primary-900/30 ml-6' : 'bg-gray-100 dark:bg-gray-700 mr-6'}`;
if (id) {
messageElement.id = id;
}
messageElement.innerHTML = `
<div class="flex items-start">
<div class="w-8 h-8 rounded-full flex items-center justify-center ${role === 'user' ? 'bg-primary-600' : 'bg-gray-600'} text-white mr-2">
<i class="fas ${role === 'user' ? 'fa-user' : 'fa-robot'} text-xs"></i>
</div>
<div class="flex-1 text-sm ${role === 'user' ? 'text-gray-800 dark:text-gray-200' : 'text-gray-700 dark:text-gray-300'}">
${content}
</div>
</div>
`;
messagesContainer.appendChild(messageElement);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
window.MindMap.assistant = assistant;
}
});
// Initialisiere den ChatGPT-Assistenten direkt
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>
<!-- Dark/Light-Mode persistent und robust -->
<!-- Dark/Light-Mode vereinheitlicht -->
<script>
(function() {
function applyMode(mode) {
if (mode === 'dark') {
document.documentElement.classList.add('dark');
localStorage.setItem('colorMode', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('colorMode', 'light');
}
}
// Beim Laden: Präferenz aus localStorage oder System übernehmen
const stored = localStorage.getItem('colorMode');
if (stored === 'dark' || stored === 'light') {
applyMode(stored);
// Globaler Zugriff für externe Skripte
window.MindMap = window.MindMap || {};
// Funktion zum Anwenden des Dark Mode, strikt getrennt
function applyDarkModeClasses(isDarkMode) {
if (isDarkMode) {
document.documentElement.classList.add('dark');
document.body.classList.add('dark');
localStorage.setItem('colorMode', 'dark');
} else {
// Systempräferenz als Fallback
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyMode(prefersDark ? 'dark' : 'light');
document.documentElement.classList.remove('dark');
document.body.classList.remove('dark');
localStorage.setItem('colorMode', 'light');
}
// Umschalter für alle Mode-Toggles
window.toggleColorMode = function() {
const isDark = document.documentElement.classList.contains('dark');
applyMode(isDark ? 'light' : 'dark');
};
// Optional: globales Event für andere Skripte
window.addEventListener('storage', function(e) {
if (e.key === 'colorMode') applyMode(e.newValue);
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
const appEl = document.querySelector('body');
if (appEl && appEl.__x) {
appEl.__x.$data.darkMode = isDarkMode;
}
// Event für andere Komponenten auslösen
document.dispatchEvent(new CustomEvent('darkModeToggled', {
detail: { isDark: isDarkMode }
}));
}
window.MindMap.toggleDarkMode = function() {
const isDark = document.documentElement.classList.contains('dark');
const newIsDark = !isDark;
// DOM aktualisieren
applyDarkModeClasses(newIsDark);
// Server aktualisieren
fetch('/api/set_dark_mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ darkMode: newIsDark })
})
.catch(console.error);
};
// Initialisierung beim Laden
document.addEventListener('DOMContentLoaded', function() {
// Reihenfolge der Prüfungen: Serverseitige Einstellung > Lokale Einstellung > Browser-Präferenz
// 1. Zuerst lokale Einstellung prüfen
const storedMode = localStorage.getItem('colorMode');
if (storedMode) {
applyDarkModeClasses(storedMode === 'dark');
} else {
// 2. Fallback auf Browser-Präferenz
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyDarkModeClasses(prefersDark);
}
// 3. Serverseitige Einstellung abrufen und anwenden
fetch('/api/get_dark_mode')
.then(response => response.json())
.then(data => {
if (data.success) {
const serverDarkMode = data.darkMode === true || data.darkMode === 'true';
applyDarkModeClasses(serverDarkMode);
}
})
.catch(error => console.error('Fehler beim Abrufen des Dark Mode Status:', error));
// Listener für Änderungen der Browser-Präferenz
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (localStorage.getItem('colorMode') === null) {
applyDarkModeClasses(e.matches);
}
});
})();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,192 @@
{% extends 'base.html' %}
{% block title %}{{ category.title }} - Forum{% endblock %}
{% block extra_css %}
<style>
.thread-item {
transition: all 0.2s ease;
}
.thread-item:hover {
transform: translateX(2px);
}
.thread-pinned {
border-left-width: 4px;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">{{ category.title }}</span>
</div>
<!-- Kategorie-Header -->
<div class="mb-8 flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center">
<!-- Kategorie-Icon -->
<div class="w-12 h-12 rounded-xl mr-4 flex items-center justify-center text-white"
style="background-color: {{ node.color_code or '#6d28d9' }}">
<i class="fas {{ node.icon or 'fa-folder' }} text-2xl"></i>
</div>
<!-- Kategorie-Info -->
<div>
<h1 class="text-2xl font-bold">{{ category.title }}</h1>
<p class="opacity-75">{{ category.description }}</p>
</div>
</div>
<!-- Neues Thema erstellen -->
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</a>
</div>
<!-- Threads anzeigen -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<!-- Header -->
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="grid grid-cols-12 gap-4">
<div class="col-span-7 font-medium">Thema</div>
<div class="col-span-1 text-center font-medium hidden md:block">Antworten</div>
<div class="col-span-2 text-center font-medium hidden md:block">Autor</div>
<div class="col-span-2 text-center font-medium hidden md:block">Letzte Antwort</div>
</div>
</div>
<!-- Thread-Liste -->
{% if threads_data %}
{% for thread_data in threads_data %}
{% set thread = thread_data.thread %}
<div class="thread-item p-4 border-b last:border-b-0 {{ 'thread-pinned' if thread.is_pinned }}"
x-bind:class="darkMode
? 'border-white/10 hover:bg-gray-700/50 {{ 'border-l-yellow-500' if thread.is_pinned }}'
: 'border-gray-200 hover:bg-gray-50 {{ 'border-l-yellow-500' if thread.is_pinned }}'">
<a href="{{ url_for('forum_post', post_id=thread.id) }}" class="block">
<div class="grid grid-cols-12 gap-4">
<!-- Thema -->
<div class="col-span-12 md:col-span-7">
<div class="flex items-start">
<!-- Status-Icons -->
<div class="flex flex-col items-center mr-3 pt-1">
{% if thread.is_pinned %}
<i class="fas fa-thumbtack text-yellow-500" title="Angepinnt"></i>
{% endif %}
{% if thread.is_locked %}
<i class="fas fa-lock text-red-500 mt-1" title="Gesperrt"></i>
{% endif %}
</div>
<!-- Themen-Info -->
<div>
<h3 class="font-medium leading-snug mb-1 {% if thread.is_locked %}opacity-70{% endif %}">
{{ thread.title }}
</h3>
<div class="flex items-center text-xs opacity-70 mt-1">
<span><i class="fas fa-eye mr-1"></i> {{ thread.view_count }}</span>
<span class="mx-2 block md:hidden"></span>
<span class="block md:hidden"><i class="fas fa-reply mr-1"></i> {{ thread_data.reply_count }}</span>
<span class="mx-2"></span>
<span><i class="fas fa-clock mr-1"></i> {{ thread.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
</div>
</div>
</div>
<!-- Antworten -->
<div class="col-span-1 text-center hidden md:flex items-center justify-center">
<span class="px-2.5 py-1 rounded-full text-sm font-medium"
x-bind:class="darkMode
? 'bg-indigo-900/40 text-indigo-300'
: 'bg-indigo-100 text-indigo-800'">
{{ thread_data.reply_count }}
</span>
</div>
<!-- Autor -->
<div class="col-span-2 text-center hidden md:flex items-center justify-center">
<div class="flex items-center">
<div class="w-7 h-7 rounded-full flex items-center justify-center text-white text-xs font-medium overflow-hidden mr-2"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if thread.author.avatar %}
<img src="{{ thread.author.avatar }}" alt="{{ thread.author.username }}" class="w-full h-full object-cover">
{% else %}
{{ thread.author.username[0].upper() }}
{% endif %}
</div>
<span class="text-sm truncate max-w-[80px]">{{ thread.author.username }}</span>
</div>
</div>
<!-- Letzte Antwort -->
<div class="col-span-2 text-center hidden md:block text-sm">
{% if thread_data.latest_reply %}
<div>{{ thread_data.latest_reply.created_at.strftime('%d.%m.%Y') }}</div>
<div class="opacity-75 text-xs">{{ thread_data.latest_reply.created_at.strftime('%H:%M') }} Uhr</div>
{% else %}
<span class="opacity-60">Keine Antworten</span>
{% endif %}
</div>
</div>
</a>
</div>
{% endfor %}
{% else %}
<div class="p-8 text-center">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Themen vorhanden</h3>
<p class="opacity-75 mb-4">In dieser Kategorie wurden noch keine Themen erstellt.</p>
<a href="{{ url_for('new_post', category_id=category.id) }}"
class="inline-block px-5 py-2.5 rounded-lg transition-all duration-300"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-plus-circle mr-2"></i>
Erstes Thema erstellen
</a>
</div>
{% endif %}
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ node.name }}</h3>
<p class="text-sm opacity-75">In der Mindmap findest du weitere Informationen zu diesem Themenbereich.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf kategoriespezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -0,0 +1,344 @@
{% extends 'base.html' %}
{% block title %}Beitrag bearbeiten{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=post.category_id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ post.category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% if post.parent_id %}
<a href="{{ url_for('forum_post', post_id=post.parent_id) }}" class="opacity-75 hover:opacity-100 transition-opacity truncate max-w-[200px]">
{{ post.parent.title }}
</a>
<span class="mx-2 opacity-50">/</span>
{% endif %}
<span class="font-medium">Beitrag bearbeiten</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Beitrag bearbeiten</h1>
<p class="opacity-75">
{% if post.parent_id %}
Antwort auf <span class="font-medium">{{ post.parent.title }}</span>
{% else %}
in der Kategorie <span class="font-medium">{{ post.category.title }}</span>
{% endif %}
</p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-2"></i>
Beitrag bearbeiten
</div>
<div class="p-6">
<form action="{{ url_for('edit_post', post_id=post.id) }}" method="POST" x-data="{
title: '{{ post.title|safe }}',
content: '{{ post.content|replace('\n', '\\n')|replace('\'', '\\\'')|safe }}',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_post', post_id=post.parent_id or post.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-save mr-2"></i>
Änderungen speichern
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% extends 'base.html' %}
{% block title %}Community Forum{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Forumskategorien -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<a href="{{ url_for('forum_category', category_id=cat_data.category.id) }}" class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 hover:bg-gray-800/80 border border-white/10' : 'bg-white hover:bg-gray-50 border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
{% if cat_data.latest_post %}
<div class="flex items-center">
<i class="fas fa-clock mr-2"></i>
<span>Neuster Beitrag: {{ cat_data.latest_post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
</div>
{% endif %}
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-chevron-right opacity-50"></i>
</div>
</div>
</div>
</a>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -0,0 +1,355 @@
{% extends 'base.html' %}
{% block title %}Neues Thema - {{ category.title }}{% endblock %}
{% block extra_css %}
<style>
.markdown-preview {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
line-height: 1.6;
}
.markdown-preview p {
margin-bottom: 1rem;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.markdown-preview h1 { font-size: 1.8rem; }
.markdown-preview h2 { font-size: 1.5rem; }
.markdown-preview h3 { font-size: 1.3rem; }
.markdown-preview h4 { font-size: 1.1rem; }
.markdown-preview ul, .markdown-preview ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-preview ul { list-style-type: disc; }
.markdown-preview ol { list-style-type: decimal; }
.markdown-preview pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.markdown-preview code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.markdown-preview pre code {
padding: 0;
background-color: transparent;
}
.markdown-preview blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.dark .markdown-preview code {
background-color: rgba(255,255,255,0.1);
}
.dark .markdown-preview blockquote {
border-color: rgba(255,255,255,0.2);
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium">Neues Thema</span>
</div>
<!-- Formular-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">Neues Thema erstellen</h1>
<p class="opacity-75">in der Kategorie <span class="font-medium">{{ category.title }}</span></p>
</div>
<!-- Formular -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-plus-circle mr-2"></i>
Neues Thema
</div>
<div class="p-6">
<form action="{{ url_for('new_post', category_id=category.id) }}" method="POST" x-data="{
title: '',
content: '',
showPreview: false,
previewHtml: '',
updatePreview() {
// Verarbeite den Inhalt
if (this.content.trim() === '') {
this.previewHtml = '<div class=\'opacity-50 italic\'>Die Vorschau wird hier angezeigt...</div>';
return;
}
// Verarbeite Markdown
let html = marked.parse(this.content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class=\'node-mention\'><i class=\'fas fa-diagram-project fa-xs mr-1\'></i>$1</span>');
this.previewHtml = html;
}
}">
<div class="mb-6">
<label for="title" class="block mb-2 font-medium">Titel des Themas</label>
<div class="rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<input type="text" id="title" name="title"
class="w-full px-4 py-3"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Ein prägnanter Titel für dein Thema"
x-model="title"
required>
</div>
</div>
<div class="mb-6">
<div class="flex justify-between items-center mb-2">
<label for="content" class="font-medium">Inhalt</label>
<div class="flex space-x-2">
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="showPreview = false"
x-bind:disabled="!showPreview"
x-bind:class="{'opacity-50': !showPreview}">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</button>
<button type="button"
class="px-3 py-1 rounded text-sm flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'"
@click="updatePreview(); showPreview = true"
x-bind:disabled="showPreview"
x-bind:class="{'opacity-50': showPreview}">
<i class="fas fa-eye mr-1"></i> Vorschau
</button>
</div>
</div>
<!-- Editor -->
<div class="rounded-lg overflow-hidden mb-2"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'"
x-show="!showPreview">
<textarea id="content" name="content" rows="12"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Schreibe deinen Beitrag hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
x-model="content"
required></textarea>
</div>
<!-- Preview -->
<div class="rounded-lg overflow-hidden mb-2 p-4 markdown-preview"
x-bind:class="darkMode
? 'border border-white/20 bg-gray-700/30'
: 'border border-gray-300 bg-gray-50'"
x-show="showPreview"
x-html="previewHtml">
</div>
<!-- Markdown-Hilfsmittel -->
<div class="mb-4" x-show="!showPreview">
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<a href="{{ url_for('forum_category', category_id=category.id) }}"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-gray-700 hover:bg-gray-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'">
Abbrechen
</a>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-paper-plane mr-2"></i>
Thema erstellen
</button>
</div>
</form>
</div>
</div>
<!-- Link zur Mindmap -->
<div class="rounded-xl p-5 mb-4 flex items-center"
x-bind:class="darkMode ? 'bg-purple-900/20 border border-purple-800/30' : 'bg-purple-50 border border-purple-100'">
<div class="text-3xl mr-4 opacity-80">
<i class="fas fa-diagram-project" style="color: {{ category.node.color_code }}"></i>
</div>
<div>
<h3 class="font-medium mb-1">Mindmap-Knotenpunkt: {{ category.node.name }}</h3>
<p class="text-sm opacity-75">Dieser Diskussionsbereich ist mit dem Mindmap-Knotenpunkt "{{ category.node.name }}" verknüpft.</p>
</div>
<div class="ml-auto">
<a href="{{ url_for('mindmap') }}"
class="px-4 py-2 rounded-lg inline-block text-sm transition-all"
x-bind:class="darkMode
? 'bg-purple-800/60 hover:bg-purple-700/60 text-white'
: 'bg-white hover:bg-purple-100 text-purple-800 border border-purple-200'">
Zur Mindmap
</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown-Buttons für den Beitragseditor
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Alpine.js Model aktualisieren
textarea.dispatchEvent(new Event('input'));
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,511 @@
{% extends 'base.html' %}
{% block title %}{{ post.title }} - Forum{% endblock %}
{% block extra_css %}
<style>
.post-content {
line-height: 1.7;
}
.post-content p {
margin-bottom: 1rem;
}
.post-content h1, .post-content h2, .post-content h3,
.post-content h4, .post-content h5, .post-content h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.post-content h1 { font-size: 1.8rem; }
.post-content h2 { font-size: 1.5rem; }
.post-content h3 { font-size: 1.3rem; }
.post-content h4 { font-size: 1.1rem; }
.post-content ul, .post-content ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.post-content ul { list-style-type: disc; }
.post-content ol { list-style-type: decimal; }
.post-content pre {
background-color: rgba(0,0,0,0.05);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1rem 0;
}
.post-content code {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: 0.3em;
background-color: rgba(0,0,0,0.05);
}
.post-content pre code {
padding: 0;
background-color: transparent;
}
.post-content blockquote {
border-left: 4px solid;
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
opacity: 0.8;
}
.post-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1rem 0;
}
.post-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.post-content th, .post-content td {
padding: 0.5rem;
border: 1px solid;
border-color: rgba(0,0,0,0.1);
}
.post-content th {
background-color: rgba(0,0,0,0.05);
}
.post-content a {
color: #6d28d9;
text-decoration: none;
}
.post-content a:hover {
text-decoration: underline;
}
.dark .post-content code {
background-color: rgba(255,255,255,0.1);
}
.dark .post-content th, .dark .post-content td {
border-color: rgba(255,255,255,0.1);
}
.dark .post-content th {
background-color: rgba(255,255,255,0.05);
}
.dark .post-content a {
color: #a78bfa;
}
.node-mention {
display: inline-flex;
align-items: center;
background-color: rgba(109, 40, 217, 0.1);
color: #6d28d9;
border-radius: 4px;
padding: 1px 6px;
font-size: 0.9em;
margin: 0 2px;
font-weight: 500;
}
.dark .node-mention {
background-color: rgba(167, 139, 250, 0.2);
color: #a78bfa;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Breadcrumb Navigation -->
<div class="mb-6 flex items-center text-sm">
<a href="{{ url_for('community') }}" class="opacity-75 hover:opacity-100 transition-opacity">
<i class="fas fa-home mr-1"></i> Forum
</a>
<span class="mx-2 opacity-50">/</span>
<a href="{{ url_for('forum_category', category_id=category.id) }}" class="opacity-75 hover:opacity-100 transition-opacity">
{{ category.title }}
</a>
<span class="mx-2 opacity-50">/</span>
<span class="font-medium truncate max-w-[300px]">{{ post.title }}</span>
</div>
<!-- Beitrags-Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold mb-2">{{ post.title }}</h1>
<div class="flex flex-wrap items-center gap-3 text-sm opacity-75">
<span><i class="fas fa-calendar-alt mr-1"></i> {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</span>
<span><i class="fas fa-eye mr-1"></i> {{ post.view_count }} Aufrufe</span>
<span><i class="fas fa-reply mr-1"></i> {{ replies|length }} Antworten</span>
{% if post.is_pinned or post.is_locked %}
<div class="flex gap-2 ml-2">
{% if post.is_pinned %}
<span class="px-2 py-0.5 text-xs rounded-full"
x-bind:class="darkMode ? 'bg-yellow-700/50 text-yellow-300' : 'bg-yellow-100 text-yellow-800'">
<i class="fas fa-thumbtack mr-1"></i> Angepinnt
</span>
{% endif %}
{% if post.is_locked %}
<span class="px-2 py-0.5 text-xs rounded-full"
x-bind:class="darkMode ? 'bg-red-700/50 text-red-300' : 'bg-red-100 text-red-800'">
<i class="fas fa-lock mr-1"></i> Gesperrt
</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Hauptbeitrag -->
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-sm'">
<!-- Beitrags-Header -->
<div class="p-4 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Autor-Avatar -->
<div class="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium text-sm overflow-hidden mr-3"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if post.author.avatar %}
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
{% else %}
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#post-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="post-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{% endif %}
</div>
<!-- Autor-Info -->
<div>
<div class="font-medium">{{ post.author.username }}</div>
<div class="text-xs opacity-70">Erstellt am {{ post.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex items-center space-x-2">
{% if current_user.id == post.user_id or current_user.role == 'admin' %}
<a href="{{ url_for('edit_post', post_id=post.id) }}"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-gray-700/50 text-gray-300'
: 'hover:bg-gray-100 text-gray-600'">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_post', post_id=post.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diesen Beitrag wirklich löschen?');">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-red-800/50 text-red-300'
: 'hover:bg-red-100 text-red-600'">
<i class="fas fa-trash-alt"></i>
</button>
</form>
{% endif %}
<!-- Moderation-Optionen -->
{% if current_user.role in ['admin', 'moderator'] %}
<div class="ml-2 border-l" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'"></div>
<form action="{{ url_for('toggle_pin_post', post_id=post.id) }}" method="POST" class="inline">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-yellow-800/50 text-yellow-300'
: 'hover:bg-yellow-100 text-yellow-600'"
title="{% if post.is_pinned %}Nicht mehr anpinnen{% else %}Anpinnen{% endif %}">
<i class="fas fa-thumbtack"></i>
</button>
</form>
<form action="{{ url_for('toggle_lock_post', post_id=post.id) }}" method="POST" class="inline">
<button type="submit"
class="p-2 rounded transition-colors"
x-bind:class="darkMode
? 'hover:bg-blue-800/50 text-blue-300'
: 'hover:bg-blue-100 text-blue-600'"
title="{% if post.is_locked %}Entsperren{% else %}Sperren{% endif %}">
<i class="fas {% if post.is_locked %}fa-unlock{% else %}fa-lock{% endif %}"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
<!-- Beitrags-Inhalt -->
<div class="p-6">
<div class="post-content markdown-content" id="main-post-content">
{{ post.content|safe }}
</div>
{% if post.updated_at and post.updated_at != post.created_at %}
<div class="mt-6 pt-4 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ post.updated_at.strftime('%d.%m.%Y, %H:%M') }}
</div>
{% endif %}
</div>
</div>
<!-- Antworten-Bereich -->
<div class="mb-8">
<h2 class="text-xl font-semibold mb-4">
<i class="fas fa-reply mr-2 opacity-60"></i>
{{ replies|length }} Antworten
</h2>
<!-- Antworten-Liste -->
{% if replies %}
{% for reply in replies %}
<div class="mb-5 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
<!-- Antwort-Header -->
<div class="p-3 border-b" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Autor-Avatar -->
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white font-medium text-xs overflow-hidden mr-3"
style="background: linear-gradient(135deg, #8b5cf6, #6366f1);">
{% if reply.author.avatar %}
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
{% else %}
<svg width="100%" height="100%" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" fill="url(#reply-avatar-gradient)" stroke="#7C3AED" stroke-width="4"/>
<circle cx="100" cy="80" r="36" fill="white"/>
<path d="M100 140C77.9086 140 60 157.909 60 180H140C140 157.909 122.091 140 100 140Z" fill="white"/>
<defs>
<linearGradient id="reply-avatar-gradient" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8B5CF6"/>
<stop offset="1" stop-color="#3B82F6"/>
</linearGradient>
</defs>
</svg>
{% endif %}
</div>
<!-- Autor-Info -->
<div>
<div class="font-medium text-sm">{{ reply.author.username }}</div>
<div class="text-xs opacity-70">{{ reply.created_at.strftime('%d.%m.%Y, %H:%M') }}</div>
</div>
</div>
<!-- Aktionen -->
<div class="flex items-center space-x-1">
{% if current_user.id == reply.user_id or current_user.role == 'admin' %}
<a href="{{ url_for('edit_post', post_id=reply.id) }}"
class="p-1.5 rounded text-sm transition-colors"
x-bind:class="darkMode
? 'hover:bg-gray-700/50 text-gray-300'
: 'hover:bg-gray-100 text-gray-600'">
<i class="fas fa-edit"></i>
</a>
<form action="{{ url_for('delete_post', post_id=reply.id) }}" method="POST" class="inline" onsubmit="return confirm('Möchtest du diese Antwort wirklich löschen?');">
<button type="submit"
class="p-1.5 rounded text-sm transition-colors"
x-bind:class="darkMode
? 'hover:bg-red-800/50 text-red-300'
: 'hover:bg-red-100 text-red-600'">
<i class="fas fa-trash-alt"></i>
</button>
</form>
{% endif %}
</div>
</div>
</div>
<!-- Antwort-Inhalt -->
<div class="p-5">
<div class="post-content markdown-content reply-content" id="reply-content-{{ reply.id }}">
{{ reply.content|safe }}
</div>
{% if reply.updated_at and reply.updated_at != reply.created_at %}
<div class="mt-4 pt-3 text-xs opacity-60 border-t" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-edit mr-1"></i> Zuletzt bearbeitet: {{ reply.updated_at.strftime('%d.%m.%Y, %H:%M') }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="rounded-xl p-6 text-center"
x-bind:class="darkMode ? 'bg-gray-800/40 border border-white/10' : 'bg-white border border-gray-200'">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-comments"></i></div>
<h3 class="text-lg font-semibold mb-2">Noch keine Antworten</h3>
<p class="opacity-75">Sei der Erste, der auf diesen Beitrag antwortet!</p>
</div>
{% endif %}
</div>
<!-- Antwort-Formular -->
{% if not post.is_locked %}
<div class="mb-8 rounded-xl overflow-hidden"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200'">
<div class="p-4 border-b font-medium" x-bind:class="darkMode ? 'border-white/10' : 'border-gray-200'">
<i class="fas fa-reply mr-2"></i>
Antworten
</div>
<div class="p-6">
<form action="{{ url_for('reply_to_post', post_id=post.id) }}" method="POST">
<div class="mb-4">
<label for="content" class="block mb-2 font-medium">Deine Antwort</label>
<div class="mb-2 rounded-lg overflow-hidden"
x-bind:class="darkMode ? 'border border-white/20' : 'border border-gray-300'">
<textarea id="content" name="content" rows="6"
class="w-full p-3 resize-y"
x-bind:class="darkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:border-indigo-500'
: 'bg-white text-gray-700 placeholder-gray-400 focus:border-indigo-500'"
placeholder="Schreibe deine Antwort hier (unterstützt Markdown und @Knotenname-Erwähnungen)..."
required></textarea>
</div>
<div class="text-xs opacity-70">
<p>Du kannst Knotenpunkte der Mindmap durch <code>@Knotenname</code> verlinken.</p>
<p>Dieser Editor unterstützt Markdown-Formatierung:</p>
<div class="flex flex-wrap gap-2 mt-1">
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="**" data-before="" data-after="" title="Fett">
<i class="fas fa-bold"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="*" data-before="" data-after="" title="Kursiv">
<i class="fas fa-italic"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="`" data-before="" data-after="" title="Code">
<i class="fas fa-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="[Link-Text](URL)" data-before="" data-after="" title="Link">
<i class="fas fa-link"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="\n```\nCode-Block\n```" data-before="" data-after="" title="Code-Block">
<i class="fas fa-file-code"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format=">" data-before="" data-after="" title="Zitat">
<i class="fas fa-quote-right"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="- " data-before="" data-after="" title="Liste">
<i class="fas fa-list-ul"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="1. " data-before="" data-after="" title="Nummerierte Liste">
<i class="fas fa-list-ol"></i>
</button>
<button type="button" class="markdown-button px-2 py-1 rounded text-xs" data-format="# " data-before="" data-after="" title="Überschrift">
<i class="fas fa-heading"></i>
</button>
</div>
</div>
</div>
<div>
<button type="submit"
class="px-5 py-2.5 rounded-lg transition-all duration-300 flex items-center"
x-bind:class="darkMode
? 'bg-indigo-700 hover:bg-indigo-600 text-white'
: 'bg-indigo-500 hover:bg-indigo-600 text-white'">
<i class="fas fa-paper-plane mr-2"></i>
Antwort senden
</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="rounded-xl p-5 text-center mb-6"
x-bind:class="darkMode ? 'bg-red-900/20 border border-red-800/30' : 'bg-red-50 border border-red-100'">
<i class="fas fa-lock mr-2 text-red-500"></i>
<span>Dieser Beitrag ist geschlossen. Es können keine neuen Antworten mehr verfasst werden.</span>
</div>
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Markdown und Knotenerwähnungen verarbeiten
const processContent = (content) => {
// Verarbeite Markdown mit marked.js
let html = marked.parse(content);
// Ersetze @Knotenname mit entsprechenden Links
html = html.replace(/@([a-zA-Z0-9äöüÄÖÜß_-]+)/g, '<span class="node-mention"><i class="fas fa-diagram-project fa-xs mr-1"></i>$1</span>');
return html;
};
// Markdown-Inhalt für Hauptbeitrag rendern
const mainPostContent = document.getElementById('main-post-content');
if (mainPostContent) {
mainPostContent.innerHTML = processContent(mainPostContent.textContent.trim());
}
// Markdown-Inhalt für Antworten rendern
document.querySelectorAll('.reply-content').forEach(reply => {
reply.innerHTML = processContent(reply.textContent.trim());
});
// Markdown-Buttons für das Antwortformular
document.querySelectorAll('.markdown-button').forEach(button => {
button.addEventListener('click', function() {
const textarea = document.getElementById('content');
const format = this.dataset.format;
const before = this.dataset.before || '';
const after = this.dataset.after || '';
// Hole die aktuelle Auswahl
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = textarea.value.substring(start, end);
// Wende die Formatierung an
let formattedText;
if (format.includes('\n')) {
// Für Formate mit Zeilenumbrüchen (z.B. Code-Blöcke)
formattedText = format.replace('Code-Block', selection || 'Code-Block');
} else if (format.includes('[Link-Text](URL)')) {
formattedText = format.replace('Link-Text', selection || 'Link-Text');
} else if (format === '- ' || format === '1. ' || format === '# ' || format === '> ') {
// Für Listen und Überschriften: am Anfang der Zeile einfügen
const beforeSelection = textarea.value.substring(0, start);
const afterSelection = textarea.value.substring(end);
// Finde den Anfang der aktuellen Zeile
const lastNewline = beforeSelection.lastIndexOf('\n');
const lineStart = lastNewline === -1 ? 0 : lastNewline + 1;
// Füge das Format am Zeilenanfang ein
formattedText = beforeSelection.substring(0, lineStart) +
format +
beforeSelection.substring(lineStart) +
selection +
afterSelection;
// Setze die neue Cursor-Position
const newCursorPos = end + format.length;
textarea.value = formattedText;
textarea.setSelectionRange(newCursorPos, newCursorPos);
return; // Früher zurückkehren, da wir die Formatierung bereits angewendet haben
} else {
// Für einfache Formatierungen wie fett, kursiv, Code
formattedText = before + format + selection + format + after;
}
// Ersetze den Text
textarea.value = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
// Setze den Fokus zurück auf das Textarea
textarea.focus();
// Setze die Auswahl neu, wenn es eine Auswahl gab
if (selection) {
const newStart = start + before.length + format.length;
const newEnd = newStart + selection.length;
textarea.setSelectionRange(newStart, newEnd);
} else {
// Setze den Cursor in die Mitte von **|** oder `|`
const newCursorPos = start + before.length + format.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,137 @@
{% extends 'base.html' %}
{% block title %}Community Forum Vorschau{% endblock %}
{% block extra_css %}
<style>
.forum-category {
transition: all 0.3s ease;
}
.forum-category:hover {
transform: translateY(-2px);
}
.category-icon {
font-size: 1.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Seitenüberschrift -->
<div class="mb-8 text-center">
<h1 class="text-3xl font-bold mb-2 gradient-text">Community Forum</h1>
<p class="text-lg opacity-75">Diskutiere mit anderen Nutzern über die Hauptthemenbereiche der Mindmap</p>
</div>
<!-- Login-Aufforderung -->
<div class="rounded-xl p-6 text-center mb-8 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-700/30">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lock mr-2 text-indigo-500"></i>
Anmeldung erforderlich
</h3>
<p class="mb-4">Um am Community-Forum teilzunehmen und alle Funktionen nutzen zu können, musst du dich anmelden oder registrieren.</p>
<div class="flex justify-center gap-4 mt-4">
<a href="{{ url_for('login', next=url_for('forum')) }}" class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg transition-colors">
<i class="fas fa-sign-in-alt mr-2"></i>Anmelden
</a>
<a href="{{ url_for('register') }}" class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg transition-colors">
<i class="fas fa-user-plus mr-2"></i>Registrieren
</a>
</div>
</div>
<!-- Forumskategorien Vorschau -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
{% if categories_data %}
{% for cat_data in categories_data %}
<div class="forum-category block">
<div class="rounded-xl p-5 h-full"
x-bind:class="darkMode ? 'bg-gray-800/60 border border-white/10' : 'bg-white border border-gray-200 shadow-md'">
<div class="flex items-start">
<!-- Kategorie-Icon -->
<div class="category-icon mr-4 text-white"
style="background-color: {{ cat_data.category.node.color_code or '#6d28d9' }}">
<i class="fas {{ cat_data.category.node.icon or 'fa-folder' }}"></i>
</div>
<!-- Kategorie-Info -->
<div class="flex-grow">
<h3 class="text-xl font-semibold mb-2">{{ cat_data.category.title }}</h3>
<p class="opacity-75 text-sm mb-3">{{ cat_data.category.description }}</p>
<!-- Statistik -->
<div class="flex flex-wrap gap-4 text-sm opacity-80">
<div class="flex items-center">
<i class="fas fa-comment-alt mr-2"></i>
<span>{{ cat_data.total_posts }} Themen</span>
</div>
<div class="flex items-center">
<i class="fas fa-reply mr-2"></i>
<span>{{ cat_data.total_replies }} Antworten</span>
</div>
</div>
</div>
<!-- Pfeil-Icon -->
<div class="ml-2">
<i class="fas fa-lock opacity-50"></i>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-span-2 text-center py-8">
<div class="text-3xl mb-3 opacity-30"><i class="fas fa-exclamation-circle"></i></div>
<h3 class="text-xl font-semibold mb-2">Keine Forum-Kategorien gefunden</h3>
<p class="opacity-75">Es sind derzeit keine Kategorien für Diskussionen verfügbar.</p>
</div>
{% endif %}
</div>
<!-- Hinweis zur Nutzung -->
<div class="rounded-xl p-6 text-center mb-8"
x-bind:class="darkMode ? 'bg-indigo-900/30 border border-indigo-700/30' : 'bg-indigo-50 border border-indigo-100'">
<h3 class="text-xl font-semibold mb-3">
<i class="fas fa-lightbulb mr-2 text-yellow-500"></i>
So funktioniert das Forum
</h3>
<p class="mb-4">Das Community-Forum ist nach den Hauptknotenpunkten der Systades-Mindmap strukturiert.
In deinen Beiträgen kannst du Knotenpunkte mit <code>@Knotenname</code> verlinken.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-users text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Fachliche Diskussionen</h4>
<p class="text-sm opacity-75">Tausche dich mit anderen zu spezifischen Themen aus</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-link text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Wissensvernetzung</h4>
<p class="text-sm opacity-75">Verknüpfe Inhalte durch Knotenreferenzen</p>
</div>
<div class="p-4 rounded-lg"
x-bind:class="darkMode ? 'bg-indigo-800/40' : 'bg-white border border-indigo-100'">
<div class="text-2xl mb-2"><i class="fas fa-markdown text-indigo-400"></i></div>
<h4 class="font-medium mb-1">Markdown Support</h4>
<p class="text-sm opacity-75">Formatiere deine Beiträge mit Markdown</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Hier können bei Bedarf forumspezifische Scripts eingefügt werden
</script>
{% endblock %}

View File

@@ -0,0 +1,365 @@
{% extends "base.html" %}
{% block title %}Mindmap erstellen{% endblock %}
{% block extra_css %}
<style>
/* Spezifische Stile für die Mindmap-Erstellungsseite */
.form-container {
background-color: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
body.dark .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.form-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.form-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
body.dark .form-input,
body.dark .form-textarea {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
body:not(.dark) .form-input,
body:not(.dark) .form-textarea {
background-color: white;
border: 1px solid #e2e8f0;
color: #334155;
}
body.dark .form-input:focus,
body.dark .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
body:not(.dark) .form-input:focus,
body:not(.dark) .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.form-switch {
display: flex;
align-items: center;
}
.form-switch input[type="checkbox"] {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.form-switch label {
cursor: pointer;
width: 50px;
height: 25px;
background: rgba(100, 116, 139, 0.3);
display: block;
border-radius: 25px;
position: relative;
margin-right: 10px;
transition: all 0.3s ease;
}
.form-switch label:after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 19px;
height: 19px;
background: #fff;
border-radius: 19px;
transition: 0.3s;
}
.form-switch input:checked + label {
background: #7c3aed;
}
.form-switch input:checked + label:after {
left: calc(100% - 3px);
transform: translateX(-100%);
}
.btn-submit {
background-color: #7c3aed;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-submit:hover {
background-color: #6d28d9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
}
.btn-cancel {
background-color: transparent;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
body.dark .btn-cancel {
color: #e2e8f0;
border-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .btn-cancel {
color: #475569;
border-color: #e2e8f0;
}
.btn-cancel:hover {
transform: translateY(-2px);
}
body.dark .btn-cancel:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) .btn-cancel:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Animation für den Seiteneintritt */
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.form-container {
animation: slideInUp 0.5s ease forwards;
}
/* Animation für Hover-Effekte */
.input-animation {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.input-animation:focus {
transform: scale(1.01);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 animate-fadeIn">
<div class="max-w-3xl mx-auto">
<!-- Titel mit Animation -->
<div class="text-center mb-8 animate-pulse">
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
Neue Mindmap erstellen
</h1>
<p class="opacity-80">Erstelle deine eigene Wissenslandkarte und organisiere deine Gedanken</p>
</div>
<div class="form-container">
<div class="form-header">
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
</div>
<div class="form-body">
<form action="{{ url_for('create_mindmap') }}" method="POST">
<div class="form-group">
<label for="name" class="form-label">Name der Mindmap</label>
<input type="text" id="name" name="name" class="form-input input-animation" required placeholder="z.B. Meine Philosophie-Mindmap">
</div>
<div class="form-group">
<label for="description" class="form-label">Beschreibung</label>
<textarea id="description" name="description" class="form-textarea input-animation" placeholder="Worum geht es in dieser Mindmap?"></textarea>
</div>
<div class="form-group">
<div class="form-switch">
<input type="checkbox" id="is_private" name="is_private" checked>
<label for="is_private"></label>
<span>Private Mindmap (nur für dich sichtbar)</span>
</div>
</div>
<div class="flex justify-between mt-6">
<a href="{{ url_for('profile') }}" class="btn-cancel">
<i class="fas fa-arrow-left"></i>
Zurück
</a>
<button type="submit" class="btn-submit">
<i class="fas fa-save"></i>
Mindmap erstellen
</button>
</div>
</form>
<!-- Mindmap-Vorschau -->
<div class="mt-8">
<h3 class="text-xl font-semibold mb-4">Vorschau</h3>
<div class="mindmap-container">
<div id="cy" class="w-full h-[400px] rounded-xl border"
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
</div>
</div>
</div>
</div>
</div>
<!-- Tipps-Sektion -->
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Erstellen einer Mindmap
</h3>
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<ul class="list-disc pl-5 space-y-2">
<li>Wähle einen prägnanten, aber aussagekräftigen Namen für deine Mindmap</li>
<li>Beginne mit einem zentralen Konzept und arbeite dich nach außen vor</li>
<li>Verwende verschiedene Farben für unterschiedliche Kategorien oder Themenbereiche</li>
<li>Füge Notizen zu Knoten hinzu, um komplexere Ideen zu erklären</li>
<li>Verknüpfe verwandte Konzepte, um Beziehungen zu visualisieren</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Einfache Animationen für die Eingabefelder
const inputs = document.querySelectorAll('.input-animation');
inputs.forEach(input => {
// Subtile Skalierung bei Fokus
input.addEventListener('focus', function() {
this.style.transform = 'scale(1.01)';
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
});
input.addEventListener('blur', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
});
});
// Formular-Absenden-Animation
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
const submitBtn = this.querySelector('.btn-submit');
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird erstellt...';
submitBtn.disabled = true;
});
// Mindmap-Vorschau initialisieren
const mindmap = new MindMap.Visualization('cy', {
enableEditing: true,
onNodeClick: function(nodeData) {
console.log("Knoten ausgewählt:", nodeData);
}
});
// Formularfelder mit Mindmap verbinden
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
// Aktualisiere Mindmap wenn sich die Eingaben ändern
nameInput.addEventListener('input', function() {
if (mindmap.cy) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', this.value || 'Neue Mindmap');
}
}
});
// Initialisiere die Mindmap
mindmap.initialize().then(() => {
console.log("Mindmap-Vorschau initialisiert");
// Setze initiale Werte
if (nameInput.value) {
const rootNode = mindmap.cy.$('#root');
if (rootNode.length > 0) {
rootNode.data('name', nameInput.value);
}
}
}).catch(error => {
console.error("Fehler bei der Initialisierung der Mindmap:", error);
});
});
</script>
{% endblock %}

525
templates/edit_mindmap.html Normal file
View File

@@ -0,0 +1,525 @@
{% extends "base.html" %}
{% block title %}Mindmap bearbeiten{% endblock %}
{% block extra_css %}
<style>
/* Spezifische Stile für die Mindmap-Bearbeitungsseite */
.form-container {
background-color: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
body.dark .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-container {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.form-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
body:not(.dark) .form-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.form-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
body.dark .form-input,
body.dark .form-textarea {
background-color: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #f1f5f9;
}
body:not(.dark) .form-input,
body:not(.dark) .form-textarea {
background-color: white;
border: 1px solid #e2e8f0;
color: #334155;
}
body.dark .form-input:focus,
body.dark .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
body:not(.dark) .form-input:focus,
body:not(.dark) .form-textarea:focus {
border-color: #7c3aed;
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
outline: none;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
.form-switch {
display: flex;
align-items: center;
}
.form-switch input[type="checkbox"] {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.form-switch label {
cursor: pointer;
width: 50px;
height: 25px;
background: rgba(100, 116, 139, 0.3);
display: block;
border-radius: 25px;
position: relative;
margin-right: 10px;
transition: all 0.3s ease;
}
.form-switch label:after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 19px;
height: 19px;
background: #fff;
border-radius: 19px;
transition: 0.3s;
}
.form-switch input:checked + label {
background: #7c3aed;
}
.form-switch input:checked + label:after {
left: calc(100% - 3px);
transform: translateX(-100%);
}
.btn-submit {
background-color: #7c3aed;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-submit:hover {
background-color: #6d28d9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(109, 40, 217, 0.2);
}
.btn-cancel {
background-color: transparent;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
body.dark .btn-cancel {
color: #e2e8f0;
border-color: rgba(255, 255, 255, 0.1);
}
body:not(.dark) .btn-cancel {
color: #475569;
border-color: #e2e8f0;
}
.btn-cancel:hover {
transform: translateY(-2px);
}
body.dark .btn-cancel:hover {
background-color: rgba(255, 255, 255, 0.05);
}
body:not(.dark) .btn-cancel:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Animation für den Seiteneintritt */
@keyframes slideInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.form-container {
animation: slideInUp 0.5s ease forwards;
}
/* Animation für Hover-Effekte */
.input-animation {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.input-animation:focus {
transform: scale(1.01);
}
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 animate-fadeIn">
<div class="max-w-3xl mx-auto">
<!-- Titel mit Animation -->
<div class="text-center mb-8 animate-pulse">
<h1 class="text-3xl font-bold mb-2 mystical-glow gradient-text">
Mindmap bearbeiten
</h1>
<p class="opacity-80">Aktualisiere die Details deiner Mindmap</p>
</div>
<div class="form-container">
<div class="form-header">
<h2 class="text-xl font-semibold">Mindmap-Details</h2>
</div>
<div class="form-body">
<form id="edit-mindmap-form">
<div class="form-group">
<label for="name" class="form-label">Name der Mindmap</label>
<input type="text" id="name" name="name" class="form-input input-animation" required
placeholder="z.B. Meine Philosophie-Mindmap" value="{{ mindmap.name }}">
</div>
<div class="form-group">
<label for="description" class="form-label">Beschreibung</label>
<textarea id="description" name="description" class="form-textarea input-animation"
placeholder="Worum geht es in dieser Mindmap?">{{ mindmap.description }}</textarea>
</div>
<div class="form-group">
<div class="form-switch">
<input type="checkbox" id="is_private" name="is_private" {% if mindmap.is_private %}checked{% endif %}>
<label for="is_private"></label>
<span>Private Mindmap (nur für dich sichtbar)</span>
</div>
</div>
<div class="flex justify-between mt-6">
<a href="{{ url_for('my_account') }}" class="btn-cancel"> {# Zurück zur Kontoübersicht geändert #}
<i class="fas fa-arrow-left"></i>
Zurück
</a>
<button type="button" id="save-mindmap-details-btn" class="btn-submit"> {# type="button" und ID hinzugefügt #}
<i class="fas fa-save"></i>
Änderungen speichern
</button>
</div>
</form>
<!-- Mindmap-Editor -->
<div class="mt-8">
<h3 class="text-xl font-semibold mb-4">Mindmap bearbeiten</h3>
<div class="mindmap-container">
<div id="cy" class="w-full h-[600px] rounded-xl border"
x-bind:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
</div>
</div>
<!-- Bearbeitungshinweise -->
<div class="mt-4 text-sm opacity-80">
<p><i class="fas fa-info-circle mr-2"></i>Klicke auf Knoten zum Bearbeiten, ziehe sie zum Neuanordnen oder nutze die Toolbar für weitere Funktionen.</p>
</div>
</div>
</div>
</div>
<!-- Tipps-Sektion -->
<div class="mt-8 p-5 rounded-lg border animate-fadeIn"
x-bind:class="darkMode ? 'bg-slate-800/40 border-slate-700/50' : 'bg-white border-slate-200'">
<h3 class="text-xl font-semibold mb-3"
x-bind:class="darkMode ? 'text-white' : 'text-gray-800'">
<i class="fa-solid fa-lightbulb text-yellow-400 mr-2"></i>Tipps zum Bearbeiten einer Mindmap
</h3>
<div x-bind:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<ul class="list-disc pl-5 space-y-2">
<li>Überprüfe, ob der Name noch zum aktuellen Inhalt passt</li>
<li>Aktualisiere die Beschreibung, um neue Aspekte zu berücksichtigen</li>
<li>Entscheide, ob die Sichtbarkeitseinstellungen noch passend sind</li>
<li>Nutze aussagekräftige Namen für bessere Auffindbarkeit</li>
<li>Behalte die Konsistenz mit verknüpften Konzepten im Auge</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-cose-bilkent/4.1.0/cytoscape-cose-bilkent.min.js"></script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Einfache Animationen für die Eingabefelder
const inputs = document.querySelectorAll('.input-animation');
inputs.forEach(input => {
// Subtile Skalierung bei Fokus
input.addEventListener('focus', function() {
this.style.transform = 'scale(1.01)';
this.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.15)';
});
input.addEventListener('blur', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
});
});
// Formular-Absenden-Logik für Metadaten
const editMindmapForm = document.getElementById('edit-mindmap-form');
const saveDetailsBtn = document.getElementById('save-mindmap-details-btn');
if (saveDetailsBtn && editMindmapForm) {
saveDetailsBtn.addEventListener('click', async function(event) {
event.preventDefault();
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
const isPrivateInput = document.getElementById('is_private');
const mindmapId = "{{ mindmap.id }}"; // Sicherstellen, dass mindmap.id hier verfügbar ist
const data = {
name: nameInput.value,
description: descriptionInput.value,
is_private: isPrivateInput.checked
// Die 'data' (Knoten/Kanten) wird separat vom Cytoscape-Editor gehandhabt
};
saveDetailsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wird gespeichert...';
saveDetailsBtn.disabled = true;
try {
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
showStatus('Metadaten erfolgreich gespeichert!', false);
// Optional: Weiterleitung oder Aktualisierung der Seiteninhalte
// window.location.href = "{{ url_for('my_account') }}";
} else {
const errorData = await response.json();
console.error('Fehler beim Speichern der Metadaten:', errorData);
showStatus(`Fehler: ${errorData.error || response.statusText}`, true);
}
} catch (error) {
console.error('Netzwerkfehler oder anderer Fehler:', error);
showStatus('Speichern fehlgeschlagen. Netzwerkproblem?', true);
} finally {
saveDetailsBtn.innerHTML = '<i class="fas fa-save"></i> Änderungen speichern';
saveDetailsBtn.disabled = false;
}
});
}
// Mindmap initialisieren
const mindmap = new MindMap.Visualization('cy', {
enableEditing: true,
apiEndpoint: '/api/mindmap/{{ mindmap.id }}',
onNodeClick: function(nodeData) {
console.log("Knoten ausgewählt:", nodeData);
},
onChange: function(dataFromCytoscape) {
// Automatisches Speichern bei Änderungen der Mindmap-Struktur
// Die Metadaten (Name, Beschreibung, is_private) werden separat über das Formular oben gespeichert.
// Diese onChange Funktion kümmert sich nur um die Strukturdaten (Knoten/Kanten).
const mindmapId = "{{ mindmap.id }}";
// Debounce-Funktion, um API-Aufrufe zu limitieren
let debounceTimer;
const debounceSaveStructure = (currentMindmapData) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
// Der Backend-Endpunkt PUT /api/mindmaps/<id> erwartet ein Objekt,
// das die zu aktualisierenden Felder enthält. Für die Struktur ist das 'data'.
const payload = {
data: currentMindmapData // Dies sind die von Cytoscape gelieferten Strukturdaten
};
// showStatus('Speichere Struktur...', false); // Status wird jetzt über Event gehandhabt
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt angepasst
method: 'PUT', // Methode zu PUT geändert
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload) // Sende die Mindmap-Daten als { data: ... }
}).then(response => {
if (!response.ok) {
response.json().then(err => {
console.error('Fehler beim Speichern der Struktur:', err);
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${err.message || err.error || 'Speicherfehler'}` } }));
}).catch(() => {
console.error('Fehler beim Speichern der Struktur, Status:', response.statusText);
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: `Struktur: ${response.statusText}` } }));
});
// throw new Error('Netzwerkfehler beim Speichern der Struktur'); // Wird schon behandelt
return; // Verhindere weitere Verarbeitung bei Fehler
}
return response.json();
}).then(responseData => {
if (responseData) { // Nur wenn response.ok war
console.log('Mindmap-Struktur erfolgreich gespeichert:', responseData);
// Die responseData von einem PUT könnte die aktualisierte Mindmap oder nur eine Erfolgsmeldung sein.
// Annahme: { message: "Mindmap updated successfully", mindmap: { ... } } oder ähnlich
document.dispatchEvent(new CustomEvent('mindmapSaved', { detail: { message: 'Struktur aktualisiert!' }}));
}
}).catch(error => {
console.error('Netzwerkfehler oder anderer Fehler beim Speichern der Struktur:', error);
// Vermeide doppelte Fehlermeldung, falls schon durch !response.ok behandelt
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
document.dispatchEvent(new CustomEvent('mindmapError', { detail: { message: 'Struktur: Netzwerkfehler' } }));
}
});
}, 1500); // Speichern 1.5 Sekunden nach der letzten Änderung
};
debounceSaveStructure(dataFromCytoscape); // Aufruf der Debounce-Funktion mit Cytoscape-Daten
}
});
// Die Verknüpfung der Formularfelder (Name, Beschreibung) mit dem Cytoscape Root-Knoten wird entfernt,
// da die Metadaten nun über das separate Formular oben gespeichert werden und nicht mehr direkt
// die Cytoscape-Daten manipulieren sollen. Die Logik für mindmap.saveToServer() wurde entfernt,
// da das Speichern jetzt über den onChange Handler mit PUT /api/mindmaps/<id> erfolgt.
// const nameInput = document.getElementById('name'); // Bereits oben deklariert für Metadaten
// nameInput.removeEventListener('input', ...); // Event Listener muss hier nicht entfernt werden, da er nicht neu hinzugefügt wird.
// Initialisiere die Mindmap mit existierenden Daten
mindmap.initialize().then(() => {
console.log("Mindmap-Editor initialisiert");
const mindmapId = "{{ mindmap.id }}";
// Lade existierende Daten für die Mindmap-Struktur
fetch(`/api/mindmaps/${mindmapId}`, { // Endpunkt für GET angepasst
method: 'GET',
headers: {
'Accept': 'application/json'
}
})
.then(response => {
if (!response.ok) {
response.json().then(err => {
showStatus(`Fehler beim Laden: ${err.message || err.error || response.statusText}`, true);
}).catch(() => {
showStatus(`Fehler beim Laden: ${response.statusText}`, true);
});
throw new Error(`Netzwerkantwort war nicht ok: ${response.statusText}`);
}
return response.json();
})
.then(mindmapDataFromServer => {
// Die API GET /api/mindmaps/<id> gibt ein Objekt zurück, das { id, name, description, is_private, data, ... } enthält.
// Wir brauchen nur den 'data'-Teil (Struktur) für Cytoscape.
// Die Metadaten (name, description, is_private) werden bereits serverseitig in die Formularfelder gerendert.
if (mindmapDataFromServer && mindmapDataFromServer.data) {
mindmap.loadData(mindmapDataFromServer.data); // Lade nur die Strukturdaten
console.log("Mindmap-Strukturdaten geladen:", mindmapDataFromServer.data);
showStatus("Mindmap geladen.", false);
} else {
console.error("Fehler: Mindmap-Daten (Struktur) nicht im erwarteten Format:", mindmapDataFromServer);
showStatus("Fehler: Mindmap-Struktur konnte nicht geladen werden (Formatfehler).", true);
}
})
.catch(error => {
console.error("Fehler beim Laden der Mindmap-Strukturdaten:", error);
if (!document.querySelector('.bg-red-500')) { // Prüft, ob schon eine Fehlermeldung angezeigt wird
showStatus("Laden der Struktur fehlgeschlagen.", true);
}
});
}).catch(error => {
console.error("Fehler bei der Initialisierung des Editors:", error);
});
// Autosave-Status Anzeige
const statusIndicator = document.createElement('div');
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300';
document.body.appendChild(statusIndicator);
// Zeige Speicherstatus
function showStatus(message, isError = false) {
statusIndicator.textContent = message;
statusIndicator.className = `fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 ${
isError
? 'bg-red-500 text-white'
: 'bg-green-500 text-white'
}`;
setTimeout(() => {
statusIndicator.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-full text-sm transition-all duration-300 opacity-0';
}, 2000);
}
// Event-Listener für Speicherstatus
document.addEventListener('mindmapSaved', (event) => {
const message = event.detail && event.detail.message ? event.detail.message : 'Erfolgreich gespeichert!';
showStatus(message, false);
});
document.addEventListener('mindmapError', (event) => {
showStatus(event.detail.message, true);
});
});
</script>
{% endblock %}

48
templates/errors/400.html Normal file
View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}400 - Ungültige Anfrage{% endblock %}
{% block content %}
<div class="container mx-auto max-w-4xl px-4 py-8">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-200 dark:border-gray-700">
<div class="text-center mb-8">
<div class="text-6xl font-bold text-red-500 mb-4">400</div>
<h1 class="text-3xl font-bold mb-2">Ungültige Anfrage</h1>
<p class="text-gray-600 dark:text-gray-400">Die Anfrage konnte nicht verarbeitet werden.</p>
</div>
<div class="mb-8 p-4 border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900/20 rounded-lg">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-400">Fehlerbeschreibung</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
{% if error %}
<p>{{ error }}</p>
{% else %}
<p>Die Anfrage enthält ungültige oder fehlerhafte Daten und konnte nicht verarbeitet werden.</p>
{% endif %}
</div>
</div>
</div>
</div>
<div class="text-center">
<p class="mb-4 text-gray-600 dark:text-gray-400">Hier sind einige Dinge, die Sie versuchen können:</p>
<ul class="list-disc list-inside text-left max-w-md mx-auto mb-6 text-gray-600 dark:text-gray-400">
<li>Überprüfen Sie Ihre Eingaben auf Fehler.</li>
<li>Stellen Sie sicher, dass Sie die richtigen Daten übermittelt haben.</li>
<li>Versuchen Sie, die Seite neu zu laden.</li>
<li>Kehren Sie zur Startseite zurück und versuchen Sie es erneut.</li>
</ul>
<a href="{{ url_for('index') }}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Zurück zur Startseite
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -8,56 +8,56 @@
<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>
<h2 class="text-xl font-bold mb-4">Angaben gemäß § 5 TMG und § 55 RStV</h2>
<p class="mb-4">
<strong>Vertreten durch:</strong><br>
Max Mustermann, Geschäftsführer
Diese Website wird privat betrieben von:<br>
Marwin Medczinski<br>
Fasanenstraße 30<br>
16761 Hennigsdorf<br>
Deutschland
</p>
</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
Telefon: +49 (0) 173 8041824<br>
E-Mail: medczinski.marwin@gmx.de
</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
<h2 class="text-xl font-bold mb-4">Inhaltlich Verantwortlicher gemäß § 55 Abs. 2 RStV</h2>
<p class="mb-4">
Marwin Medczinski<br>
Fasanenstraße 30<br>
16761 Hennigsdorf
</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Hinweis zur Streitbeilegung</h2>
<p class="mb-4">Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: <a href="https://ec.europa.eu/consumers/odr/" target="_blank" class="text-purple-600 hover:text-purple-800">https://ec.europa.eu/consumers/odr/</a></p>
<p class="mb-4">Ich bin nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</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>
<p class="mb-4">Die Inhalte dieser Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann ich jedoch keine Gewähr übernehmen. Als Diensteanbieter bin ich gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG bin ich 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 werde ich 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>
<p class="mb-4">Diese Website enthält Links zu externen Webseiten Dritter, auf deren Inhalte ich keinen Einfluss habe. Deshalb kann ich 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 class="mb-4">Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Links umgehend entfernen.</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Urheberrecht</h2>
<p class="mb-4">Die durch mich erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen meiner schriftlichen Zustimmung. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.</p>
<p>Soweit die Inhalte auf dieser Seite nicht von mir erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitte ich um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Inhalte umgehend entfernen.</p>
</section>
</div>
</div>

View File

@@ -39,6 +39,35 @@
animation: textReveal 1s cubic-bezier(0.77, 0, 0.18, 1) forwards;
}
/* Marker-Animation für den Text */
@keyframes markerAnimation {
0% { width: 0; opacity: 0; }
20% { width: 100%; opacity: 0.7; }
80% { width: 100%; opacity: 0.7; }
100% { width: 0; opacity: 0; }
}
.marker-animation {
position: relative;
}
.marker-animation::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 6px;
width: 0;
background: linear-gradient(to right, rgba(109, 40, 217, 0.3), rgba(139, 92, 246, 0.6), rgba(109, 40, 217, 0.3));
border-radius: 3px;
opacity: 0;
animation: markerAnimation 2.5s ease-in-out forwards;
}
.marker-animation-delay::after {
animation-delay: 1.5s;
}
.delay-1 { animation-delay: 0.2s; }
.delay-2 { animation-delay: 0.4s; }
.delay-3 { animation-delay: 0.6s; }
@@ -71,16 +100,20 @@
/* Chat section styles */
.embedded-chat {
height: 350px;
height: 500px;
border-radius: 1rem;
overflow: hidden;
transition: all 0.3s ease;
border: 1px solid;
display: flex;
flex-direction: column;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 5px 10px -5px rgba(0, 0, 0, 0.05);
}
.dark .embedded-chat {
background-color: rgba(17, 24, 39, 0.7);
border-color: rgba(109, 40, 217, 0.2);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 5px 10px -5px rgba(0, 0, 0, 0.2);
}
.embedded-chat {
@@ -89,9 +122,118 @@
}
#embedded-chat-messages {
height: 250px;
flex: 1;
overflow-y: auto;
padding: 1.25rem;
min-height: 320px;
}
.chat-input-container {
padding: 1.25rem;
border-top: 1px solid;
background-color: rgba(255, 255, 255, 0.3);
}
.dark .chat-input-container {
background-color: rgba(17, 24, 39, 0.6);
border-color: rgba(75, 85, 99, 0.4);
}
.mystical-input {
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(209, 213, 219, 0.5);
color: #4B5563;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
outline: none;
transition: all 0.3s ease;
width: 100%;
font-size: 1rem;
}
.dark .mystical-input {
background-color: rgba(31, 41, 55, 0.7);
border-color: rgba(75, 85, 99, 0.4);
color: #E5E7EB;
}
.mystical-input:focus {
border-color: rgba(139, 92, 246, 0.5);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
.dark .mystical-input:focus {
border-color: rgba(139, 92, 246, 0.5);
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
}
/* Verbesserte Lesbarkeit für Chat-Nachrichten */
.chat-message {
margin-bottom: 1.25rem;
}
.chat-bubble {
padding: 1rem;
border-radius: 0.75rem;
max-width: 85%;
font-size: 1rem;
line-height: 1.5;
}
.assistant-bubble {
background-color: rgba(243, 244, 246, 0.95);
color: #374151;
}
.dark .assistant-bubble {
background-color: rgba(31, 41, 55, 0.95);
color: #E5E7EB;
}
.user-bubble {
background-color: rgba(139, 92, 246, 0.15);
color: #4B5563;
}
.dark .user-bubble {
background-color: rgba(124, 58, 237, 0.3);
color: #E5E7EB;
}
/* Beispiel-Buttons verbessert */
.quick-query-container {
margin-top: 0.75rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.quick-query-btn {
font-size: 0.8rem;
padding: 0.4rem 0.75rem;
border-radius: 2rem;
background-color: rgba(243, 244, 246, 0.8);
color: #4B5563;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
border: 1px solid rgba(209, 213, 219, 0.5);
}
.dark .quick-query-btn {
background-color: rgba(55, 65, 81, 0.8);
color: #E5E7EB;
border-color: rgba(75, 85, 99, 0.4);
}
.quick-query-btn:hover {
background-color: rgba(229, 231, 235, 0.9);
transform: translateY(-1px);
}
.dark .quick-query-btn:hover {
background-color: rgba(75, 85, 99, 0.9);
}
/* Chat typing indicator */
@@ -131,16 +273,9 @@
<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">
<div class="overflow-hidden">
<span class="gradient-text inline-block text-reveal">Wissen</span>
</div>
<div class="overflow-hidden mt-2">
<span class="inline-block text-reveal delay-1">neu</span>
</div>
<div class="mt-2 relative overflow-hidden">
<span class="relative inline-block text-reveal delay-2">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 class="overflow-hidden flex justify-center gap-6">
<span class="relative inline-block text-reveal marker-animation">Wissen.</span>
<span class="relative inline-block text-reveal delay-1 marker-animation marker-animation-delay">Vernetzen.</span>
</div>
</h1>
<div class="overflow-hidden">
@@ -179,6 +314,12 @@
<div class="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>
<!-- Animierte Pfeilspitze -->
<div class="mt-6 flex justify-center">
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-white animate-bounce-slow">
<path d="M10 12L0 2L2 0L10 8L18 0L20 2L10 12Z" fill="currentColor" fill-opacity="0.7"/>
</svg>
</div>
</div>
</div>
</div>
@@ -271,14 +412,14 @@
</div>
<!-- Chat Messages -->
<div id="embedded-chat-messages" class="border-b border-gray-200 dark:border-gray-700">
<div id="embedded-chat-messages" class="border-b-0">
<!-- Assistant Message -->
<div class="mb-4 flex">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
<div class="chat-message flex">
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
<i class="fa-solid fa-robot text-sm"></i>
</div>
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
<div class="text-gray-700 dark:text-gray-300 markdown-content">
<div class="chat-bubble assistant-bubble">
<div class="markdown-content">
<p>Hallo! Ich bin dein Systades-Assistent. Wie kann ich dir heute helfen?</p>
<p>Du kannst mir Fragen zu:</p>
<ul>
@@ -292,24 +433,24 @@
</div>
<!-- User Message -->
<div class="mb-4 flex justify-end">
<div class="bg-purple-100 dark:bg-purple-900/30 rounded-lg p-3 max-w-[80%]">
<p class="text-gray-800 dark:text-gray-200">
<div class="chat-message flex justify-end">
<div class="chat-bubble user-bubble">
<p>
Kann ich mit deiner Hilfe eine Mindmap zum Thema Künstliche Intelligenz erstellen?
</p>
</div>
<div class="w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-2 flex-shrink-0">
<div class="w-9 h-9 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-gray-700 dark:text-gray-300 ml-3 flex-shrink-0">
<i class="fa-solid fa-user text-sm"></i>
</div>
</div>
<!-- Assistant Response -->
<div class="mb-4 flex" id="demo-ai-response">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-2 flex-shrink-0">
<div class="chat-message flex" id="demo-ai-response">
<div class="w-9 h-9 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 flex items-center justify-center text-white mr-3 flex-shrink-0">
<i class="fa-solid fa-robot text-sm"></i>
</div>
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 max-w-[80%]">
<div class="text-gray-700 dark:text-gray-300 markdown-content">
<div class="chat-bubble assistant-bubble">
<div class="markdown-content">
<p>Ja, natürlich! Ich kann dir dabei helfen, eine Mindmap zum Thema <strong>Künstliche Intelligenz</strong> zu erstellen.</p>
<p>Du kannst wie folgt vorgehen:</p>
<ol>
@@ -325,19 +466,19 @@
</div>
<!-- Chat Input -->
<div class="p-4">
<div class="chat-input-container">
<div class="flex">
<input type="text" placeholder="Stelle eine Frage..." class="mystical-input flex-grow" disabled>
<button class="ml-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white p-2 rounded-lg disabled:opacity-50" disabled>
<button class="ml-3 bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-3 py-2 rounded-lg disabled:opacity-50 flex-shrink-0 hover:shadow-md transition-all duration-200" disabled>
<i class="fa-solid fa-paper-plane"></i>
</button>
</div>
<!-- Quick Queries -->
<div class="mt-3 flex flex-wrap gap-2">
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">KI-Grundlagen</button>
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Mindmap erstellen</button>
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 cursor-pointer transition-colors">Datenbank durchsuchen</button>
<div class="quick-query-container">
<span class="text-sm text-gray-500 dark:text-gray-400 mr-1">Beispiele:</span>
<button data-question="Was sind die wichtigsten Grundlagen der Künstlichen Intelligenz?" class="quick-query-btn hover:shadow-sm">KI-Grundlagen</button>
<button data-question="Wie kann ich eine Mindmap zum Thema Neuronale Netzwerke erstellen?" class="quick-query-btn hover:shadow-sm">Mindmap erstellen</button>
<button data-question="Zeige mir alle verfügbaren Kategorien in der Datenbank" class="quick-query-btn hover:shadow-sm">Datenbank durchsuchen</button>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -84,6 +84,23 @@
</div>
</div>
</div>
<!-- Meine erstellten Mindmaps -->
<div class="mb-12">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800 dark:text-white flex items-center">
<i class="fas fa-brain mr-3 text-green-500"></i>
Meine erstellten Mindmaps
</h2>
<button id="create-mindmap-btn" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors flex items-center">
<i class="fas fa-plus mr-2"></i> Neue Mindmap erstellen
</button>
</div>
<div id="user-mindmaps-container" class="space-y-4">
<!-- Hier werden die Mindmaps des Benutzers geladen -->
<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>
</div>
</div>
<!-- Gemerkte Inhalte -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
@@ -123,6 +140,431 @@
</div>
</div>
<!-- Modal zum Erstellen einer neuen Mindmap -->
<div id="create-mindmap-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center hidden z-50">
<div class="relative mx-auto p-5 border w-full max-w-md shadow-lg rounded-md bg-white dark:bg-gray-800">
<div class="mt-3 text-center">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Neue Mindmap erstellen</h3>
<div class="mt-2 px-7 py-3">
<form id="create-mindmap-form">
<div class="mb-4">
<label for="mindmap-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Name</label>
<input type="text" name="name" id="mindmap-name" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white" required>
</div>
<div class="mb-4">
<label for="mindmap-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Beschreibung (optional)</label>
<textarea name="description" id="mindmap-description" rows="3" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"></textarea>
</div>
</form>
</div>
<div class="items-center px-4 py-3">
<button id="submit-create-mindmap" class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300">
Erstellen
</button>
<button id="cancel-create-mindmap" class="mt-2 px-4 py-2 bg-gray-300 text-gray-800 dark:bg-gray-600 dark:text-gray-200 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-400 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-300">
Abbrechen
</button>
</div>
</div>
</div>
</div>
<!-- JavaScript für persönliche Mindmap und CRUD -->
<script>
</script>
<script nonce="{{ csp_nonce }}">
document.addEventListener('DOMContentLoaded', function() {
// Modal-Logik
const createMindmapBtn = document.getElementById('create-mindmap-btn');
const createMindmapModal = document.getElementById('create-mindmap-modal');
const cancelCreateMindmapBtn = document.getElementById('cancel-create-mindmap');
const submitCreateMindmapBtn = document.getElementById('submit-create-mindmap');
const createMindmapForm = document.getElementById('create-mindmap-form');
if (createMindmapBtn) {
createMindmapBtn.addEventListener('click', () => {
createMindmapModal.classList.remove('hidden');
});
}
if (cancelCreateMindmapBtn) {
cancelCreateMindmapBtn.addEventListener('click', () => {
createMindmapModal.classList.add('hidden');
createMindmapForm.reset();
});
}
// Schließen bei Klick außerhalb des Modals
if (createMindmapModal) {
createMindmapModal.addEventListener('click', (event) => {
if (event.target === createMindmapModal) {
createMindmapModal.classList.add('hidden');
createMindmapForm.reset();
}
});
}
// Funktion zum Anzeigen von Benachrichtigungen
function showNotification(message, type = 'success') {
const notificationArea = document.getElementById('notification-area') || createNotificationArea();
const notificationId = `notif-${Date.now()}`;
constbgColor = type === 'success' ? 'bg-green-500' : (type === 'error' ? 'bg-red-500' : 'bg-blue-500');
const notificationElement = `
<div id="${notificationId}" class="p-4 mb-4 text-sm text-white rounded-lg ${bgColor} animate-fadeIn" role="alert">
<span class="font-medium">${type.charAt(0).toUpperCase() + type.slice(1)}:</span> ${message}
</div>
`;
notificationArea.insertAdjacentHTML('beforeend', notificationElement);
setTimeout(() => {
const el = document.getElementById(notificationId);
if (el) {
el.classList.add('animate-fadeOut');
setTimeout(() => el.remove(), 500);
}
}, 5000);
}
function createNotificationArea() {
const area = document.createElement('div');
area.id = 'notification-area';
area.className = 'fixed top-5 right-5 z-50 w-auto max-w-sm';
document.body.appendChild(area);
// Add some basic animation styles
const style = document.createElement('style');
style.textContent = `
.animate-fadeIn { animation: fadeIn 0.5s ease-out; }
.animate-fadeOut { animation: fadeOut 0.5s ease-in forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } }
`;
document.head.appendChild(style);
return area;
}
// CRUD-Funktionen für UserMindmaps
const mindmapsContainer = document.getElementById('user-mindmaps-container');
async function fetchUserMindmaps() {
if (!mindmapsContainer) return;
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Lade Mindmaps...</p>';
try {
const response = await fetch('/api/mindmaps');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const mindmaps = await response.json();
renderMindmaps(mindmaps);
} catch (error) {
console.error('Fehler beim Laden der Mindmaps:', error);
mindmapsContainer.innerHTML = '<p class="text-red-500">Fehler beim Laden der Mindmaps.</p>';
showNotification('Fehler beim Laden der Mindmaps.', 'error');
}
}
function renderMindmaps(mindmaps) {
if (!mindmapsContainer) return;
if (mindmaps.length === 0) {
mindmapsContainer.innerHTML = '<p class="text-gray-600 dark:text-gray-400">Du hast noch keine eigenen Mindmaps erstellt.</p>';
return;
}
mindmapsContainer.innerHTML = ''; // Container leeren
const ul = document.createElement('ul');
ul.className = 'space-y-3';
mindmaps.forEach(mindmap => {
const li = document.createElement('li');
li.className = 'p-4 rounded-xl bg-white/80 dark:bg-gray-800/80 shadow-sm hover:shadow-md transition-all flex justify-between items-center';
const mindmapLink = document.createElement('a');
mindmapLink.href = `/user_mindmap/${mindmap.id}`;
mindmapLink.className = 'flex-grow';
const textDiv = document.createElement('div');
const nameH3 = document.createElement('h3');
nameH3.className = 'font-semibold text-gray-900 dark:text-white';
nameH3.textContent = mindmap.name;
textDiv.appendChild(nameH3);
if (mindmap.description) {
const descP = document.createElement('p');
descP.className = 'text-sm text-gray-600 dark:text-gray-400';
descP.textContent = mindmap.description;
textDiv.appendChild(descP);
}
mindmapLink.appendChild(textDiv);
li.appendChild(mindmapLink);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex space-x-2 ml-4';
const editButton = document.createElement('a');
editButton.href = `/edit_mindmap/${mindmap.id}`; // oder JavaScript-basiertes Editieren
editButton.className = 'px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm flex items-center';
editButton.innerHTML = '<i class="fas fa-edit mr-1"></i> Bearbeiten';
// Hier könnte auch ein Event-Listener für ein Modal zum Bearbeiten hinzugefügt werden
// editButton.addEventListener('click', (e) => { e.preventDefault(); openEditModal(mindmap); });
actionsDiv.appendChild(editButton);
const deleteButton = document.createElement('button');
deleteButton.className = 'px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 text-sm flex items-center delete-mindmap-btn';
deleteButton.innerHTML = '<i class="fas fa-trash mr-1"></i> Löschen';
deleteButton.dataset.mindmapId = mindmap.id;
actionsDiv.appendChild(deleteButton);
li.appendChild(actionsDiv);
ul.appendChild(li);
});
mindmapsContainer.appendChild(ul);
// Event Listener für Löschen-Buttons hinzufügen
document.querySelectorAll('.delete-mindmap-btn').forEach(button => {
button.addEventListener('click', async (event) => {
const mindmapId = event.currentTarget.dataset.mindmapId;
if (confirm('Bist du sicher, dass du diese Mindmap löschen möchtest?')) {
try {
const response = await fetch(`/api/mindmaps/${mindmapId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
showNotification('Mindmap erfolgreich gelöscht.', 'success');
fetchUserMindmaps(); // Liste aktualisieren
} catch (error) {
console.error('Fehler beim Löschen der Mindmap:', error);
showNotification(`Fehler beim Löschen: ${error.message}`, 'error');
}
}
});
});
}
if (submitCreateMindmapBtn) {
submitCreateMindmapBtn.addEventListener('click', async () => {
const name = document.getElementById('mindmap-name').value;
const description = document.getElementById('mindmap-description').value;
if (!name.trim()) {
showNotification('Der Name der Mindmap darf nicht leer sein.', 'error');
return;
}
try {
const response = await fetch('/api/mindmaps', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, description, is_private: false }), // is_private standardmäßig auf false setzen
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const newMindmap = await response.json();
showNotification(`Mindmap "${newMindmap.name}" erfolgreich erstellt. Weiterleitung...`, 'success');
createMindmapModal.classList.add('hidden');
createMindmapForm.reset();
// fetchUserMindmaps(); // Liste wird auf der neuen Seite ohnehin neu geladen oder ist nicht direkt sichtbar.
// Weiterleitung zur Bearbeitungsseite der neuen Mindmap
window.location.href = `/edit_mindmap/${newMindmap.id}`;
} catch (error) {
console.error('Fehler beim Erstellen der Mindmap:', error);
showNotification(`Fehler beim Erstellen: ${error.message}`, 'error');
}
});
}
// Initiale Ladefunktion für Mindmaps
fetchUserMindmaps();
// Bestehendes Skript für Bookmarks etc.
// 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
const emptyMindmapMessage = document.getElementById('empty-mindmap-message');
if (emptyMindmapMessage) {
emptyMindmapMessage.style.display = 'none';
}
// Initialisiere die persönliche Mindmap
const personalMindmapContainer = document.getElementById('personal-mindmap');
if (personalMindmapContainer && typeof MindMapVisualization !== 'undefined') {
const personalMindmap = new MindMapVisualization('#personal-mindmap', {
width: personalMindmapContainer.clientWidth,
height: 400,
nodeRadius: 18,
selectedNodeRadius: 22,
linkDistance: 120,
chargeStrength: -800,
centerForce: 0.1,
tooltipEnabled: true
});
// Lade Daten für die Mindmap
window.setTimeout(() => {
if (window.mindmapInstance) {
const nodes = window.mindmapInstance.nodes.filter(node =>
bookmarkedNodeIds.includes(node.id)
);
const links = window.mindmapInstance.links.filter(link =>
bookmarkedNodeIds.includes(link.source.id || link.source) &&
bookmarkedNodeIds.includes(link.target.id || link.target)
);
personalMindmap.nodes = nodes;
personalMindmap.links = links;
personalMindmap.isLoading = false;
personalMindmap.updateVisualization();
} else {
if (emptyMindmapMessage) emptyMindmapMessage.style.display = 'flex';
}
}, 800);
}
loadBookmarkedContent(bookmarkedNodeIds);
} else {
// Zeige Leerzustand an
const areasContainer = document.getElementById('bookmarked-areas-container');
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
if (areasContainer) {
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>
`;
}
if (thoughtsContainer) {
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 (bleibt größtenteils gleich)
function loadBookmarkedContent(nodeIds) {
if (!nodeIds || nodeIds.length === 0) return;
const areasContainer = document.getElementById('bookmarked-areas-container');
const thoughtsContainer = document.getElementById('bookmarked-thoughts-container');
const colors = ['purple', 'blue', 'green', 'indigo', 'amber'];
if (areasContainer) areasContainer.innerHTML = '';
if (thoughtsContainer) thoughtsContainer.innerHTML = '';
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 }
];
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' }
];
const areaCount = Math.min(nodeIds.length, 5);
if (areasContainer && areaCount > 0) {
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 if (areasContainer) {
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>
`;
}
const thoughtCount = Math.min(nodeIds.length, 5);
if (thoughtsContainer && thoughtCount > 0) {
for (let i = 0; i < thoughtCount; i++) {
const thought = thoughtTemplates[i];
const colorClass = colors[(i + 2) % colors.length];
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 if (thoughtsContainer) {
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>
<!-- JavaScript für persönliche Mindmap -->
<script>
document.addEventListener('DOMContentLoaded', function() {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}Einfaches Profil{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-10">
<div class="bg-gray-800 bg-opacity-70 rounded-lg p-6 mb-6">
<h1 class="text-3xl font-bold text-purple-400 mb-4">Hallo, {{ user.username }}</h1>
<div class="text-gray-300 mb-4">
<p>E-Mail: {{ user.email }}</p>
<p>Mitglied seit: {{ user.created_at.strftime('%d.%m.%Y') }}</p>
</div>
<h2 class="text-xl font-semibold text-purple-300 mt-6 mb-3">Deine Mindmaps</h2>
{% if user_mindmaps %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{% for mindmap in user_mindmaps %}
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ mindmap.name }}</h3>
<p class="text-gray-300 text-sm mb-3">{{ mindmap.description }}</p>
<div class="flex justify-between text-xs text-gray-400">
<span>Erstellt: {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
</div>
<div class="mt-4 flex justify-between">
<a href="{{ url_for('mindmap') }}?id={{ mindmap.id }}" class="text-purple-400 hover:text-purple-300">
<i class="fas fa-eye mr-1"></i> Anzeigen
</a>
<a href="{{ url_for('edit_mindmap', mindmap_id=mindmap.id) }}" class="text-blue-400 hover:text-blue-300">
<i class="fas fa-edit mr-1"></i> Bearbeiten
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-6">
<p class="text-gray-400">Du hast noch keine Mindmaps erstellt</p>
<a href="{{ url_for('create_mindmap') }}" class="mt-3 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
Erste Mindmap erstellen
</a>
</div>
{% endif %}
<h2 class="text-xl font-semibold text-purple-300 mt-8 mb-3">Deine Gedanken</h2>
{% if thoughts %}
<div class="space-y-4">
{% for thought in thoughts %}
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-medium text-purple-400 mb-2">{{ thought.title }}</h3>
<p class="text-gray-300 text-sm mb-2">
{{ thought.abstract[:150] ~ '...' if thought.abstract and thought.abstract|length > 150 else thought.abstract }}
</p>
<div class="flex justify-between text-xs text-gray-400">
<span>Erstellt: {{ thought.created_at.strftime('%d.%m.%Y') }}</span>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-6">
<p class="text-gray-400">Du hast noch keine Gedanken erstellt</p>
</div>
{% endif %}
<div class="mt-8 flex justify-between">
<a href="{{ url_for('index') }}" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600">
Zurück zur Startseite
</a>
<a href="{{ url_for('logout') }}" class="px-4 py-2 bg-red-700 text-white rounded-lg hover:bg-red-600">
Abmelden
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,512 @@
{% extends "base.html" %}
{% block title %}Entdecken{% endblock %}
{% block content %}
<div class="min-h-screen"
x-data="{
activeTab: 'users',
users: [],
posts: [],
trending: [],
loading: false,
searchQuery: '',
searchResults: [],
searching: false,
async loadUsers() {
this.loading = true;
try {
const response = await fetch('/api/discover/users');
const data = await response.json();
if (data.success) {
this.users = data.users;
}
} catch (error) {
console.error('Error loading users:', error);
} finally {
this.loading = false;
}
},
async loadPosts() {
this.loading = true;
try {
const response = await fetch('/api/discover/posts');
const data = await response.json();
if (data.success) {
this.posts = data.posts;
}
} catch (error) {
console.error('Error loading posts:', error);
} finally {
this.loading = false;
}
},
async loadTrending() {
this.loading = true;
try {
const response = await fetch('/api/discover/trending');
const data = await response.json();
if (data.success) {
this.trending = data.trending;
}
} catch (error) {
console.error('Error loading trending:', error);
} finally {
this.loading = false;
}
},
async follow(userId, index) {
try {
const response = await fetch(`/api/users/${userId}/follow`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.users[index].is_following = data.is_following;
this.users[index].follower_count = data.follower_count;
}
} catch (error) {
console.error('Error following user:', error);
}
},
async searchUsers() {
if (!this.searchQuery.trim()) {
this.searchResults = [];
return;
}
this.searching = true;
try {
const response = await fetch(`/api/search/users?q=${encodeURIComponent(this.searchQuery)}`);
const data = await response.json();
if (data.success) {
this.searchResults = data.users;
}
} catch (error) {
console.error('Error searching users:', error);
} finally {
this.searching = false;
}
},
formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
},
init() {
this.loadUsers();
}
}"
x-init="init()">
<!-- Header Section -->
<div class="border-b"
:class="darkMode ? 'border-gray-700 bg-gray-900/50' : 'border-gray-200 bg-white/50'">
<div class="max-w-6xl mx-auto px-4 py-6">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold"
:class="darkMode ? 'text-white' : 'text-gray-900'">
Entdecken
</h1>
<p class="text-lg mt-1"
:class="darkMode ? 'text-gray-400' : 'text-gray-600'">
Finde neue Leute und interessante Inhalte
</p>
</div>
<!-- Search Bar -->
<div class="flex-1 max-w-md ml-8">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-search"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'></i>
</div>
<input
x-model="searchQuery"
@input.debounce.300ms="searchUsers()"
type="text"
class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-xl leading-5 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500"
:class="darkMode
? 'bg-gray-800 border-gray-600 text-white placeholder-gray-400'
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-500'"
placeholder="Nutzer suchen...">
</div>
<!-- Search Results Dropdown -->
<div x-show="searchQuery && searchResults.length > 0"
x-transition
class="absolute z-50 mt-2 w-full rounded-xl shadow-lg border"
:class="darkMode
? 'bg-gray-800 border-gray-600'
: 'bg-white border-gray-200'">
<template x-for="user in searchResults.slice(0, 5)">
<div class="p-3 hover:bg-gray-50 transition-colors duration-150 cursor-pointer"
:class="darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-50'"
@click="window.location.href = `/profile/${user.username}`">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center text-white font-semibold">
<span x-text="user.username.charAt(0).toUpperCase()"></span>
</div>
<div>
<p class="font-medium"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="user.display_name || user.username"></p>
<p class="text-sm"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'"
x-text="`@${user.username}`"></p>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="flex space-x-1 bg-gray-100 rounded-xl p-1"
:class="darkMode ? 'bg-gray-800' : 'bg-gray-100'">
<button @click="activeTab = 'users'; loadUsers()"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-200"
:class="activeTab === 'users'
? (darkMode ? 'bg-purple-600 text-white shadow-lg' : 'bg-white text-purple-600 shadow-md')
: (darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900')">
<i class="fas fa-users mr-2"></i>
Nutzer
</button>
<button @click="activeTab = 'posts'; loadPosts()"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-200"
:class="activeTab === 'posts'
? (darkMode ? 'bg-purple-600 text-white shadow-lg' : 'bg-white text-purple-600 shadow-md')
: (darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900')">
<i class="fas fa-fire mr-2"></i>
Beliebte Posts
</button>
<button @click="activeTab = 'trending'; loadTrending()"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-200"
:class="activeTab === 'trending'
? (darkMode ? 'bg-purple-600 text-white shadow-lg' : 'bg-white text-purple-600 shadow-md')
: (darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900')">
<i class="fas fa-trending-up mr-2"></i>
Im Trend
</button>
</div>
</div>
</div>
<!-- Content Section -->
<div class="max-w-6xl mx-auto px-4 py-8">
<!-- Loading State -->
<div x-show="loading" class="text-center py-16">
<div class="inline-flex items-center space-x-3"
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<svg class="animate-spin h-8 w-8" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-lg">Lädt...</span>
</div>
</div>
<!-- Users Tab -->
<div x-show="activeTab === 'users' && !loading">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<template x-for="(user, index) in users" :key="user.id">
<div class="rounded-2xl p-6 shadow-lg border transition-all duration-300 hover:shadow-xl transform hover:scale-105"
:class="darkMode
? 'bg-gray-800/90 border-gray-700/50 backdrop-blur-xl'
: 'bg-white/80 border-gray-200/50 backdrop-blur-xl'">
<!-- User Avatar -->
<div class="text-center mb-4">
<div class="w-20 h-20 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center text-white font-bold text-2xl mx-auto shadow-lg">
<span x-text="user.username.charAt(0).toUpperCase()"></span>
</div>
</div>
<!-- User Info -->
<div class="text-center mb-4">
<h3 class="text-xl font-bold mb-1"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="user.display_name || user.username"></h3>
<p class="text-sm mb-2"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'"
x-text="`@${user.username}`"></p>
<div x-show="user.bio" class="mb-3">
<p class="text-sm"
:class="darkMode ? 'text-gray-300' : 'text-gray-700'"
x-text="user.bio"></p>
</div>
</div>
<!-- User Stats -->
<div class="flex justify-center space-x-6 mb-4">
<div class="text-center">
<div class="text-lg font-bold"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="formatNumber(user.follower_count || 0)"></div>
<div class="text-xs"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
Follower
</div>
</div>
<div class="text-center">
<div class="text-lg font-bold"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="formatNumber(user.following_count || 0)"></div>
<div class="text-xs"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
Folge ich
</div>
</div>
<div class="text-center">
<div class="text-lg font-bold"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="formatNumber(user.post_count || 0)"></div>
<div class="text-xs"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
Posts
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex space-x-2">
<button @click="follow(user.id, index)"
class="flex-1 py-2 px-4 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
:class="user.is_following
? (darkMode ? 'bg-gray-700 text-white border border-gray-600 hover:bg-gray-600' : 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200')
: 'bg-gradient-to-r from-purple-500 to-indigo-500 text-white shadow-md hover:shadow-lg'">
<span x-text="user.is_following ? 'Entfolgen' : 'Folgen'"></span>
</button>
<button @click="window.location.href = `/profile/${user.username}`"
class="px-4 py-2 rounded-xl transition-all duration-300 transform hover:scale-105"
:class="darkMode
? 'bg-gray-700 text-white border border-gray-600 hover:bg-gray-600'
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'">
<i class="fas fa-user"></i>
</button>
</div>
</div>
</template>
</div>
<!-- Empty State for Users -->
<div x-show="!loading && users.length === 0" class="text-center py-16">
<div class="mb-6">
<i class="fas fa-users text-6xl mb-4"
:class="darkMode ? 'text-gray-600' : 'text-gray-300'"></i>
</div>
<h3 class="text-2xl font-bold mb-2"
:class="darkMode ? 'text-white' : 'text-gray-900'">
Keine Nutzer gefunden
</h3>
<p class="text-lg"
:class="darkMode ? 'text-gray-400' : 'text-gray-600'">
Versuche es später noch einmal oder ändere deine Suchkriterien.
</p>
</div>
</div>
<!-- Popular Posts Tab -->
<div x-show="activeTab === 'posts' && !loading">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<template x-for="post in posts" :key="post.id">
<article class="rounded-2xl p-6 shadow-lg border transition-all duration-300 hover:shadow-xl"
:class="darkMode
? 'bg-gray-800/90 border-gray-700/50 backdrop-blur-xl'
: 'bg-white/80 border-gray-200/50 backdrop-blur-xl'">
<!-- Post Header -->
<div class="flex items-center space-x-3 mb-4">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-lg shadow-md">
<span x-text="post.author.username.charAt(0).toUpperCase()"></span>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2">
<h4 class="font-semibold truncate"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="post.author.display_name || post.author.username"></h4>
<span x-show="post.author.is_verified"
class="text-blue-500">
<i class="fas fa-check-circle text-sm"></i>
</span>
</div>
<p class="text-sm"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'"
x-text="formatTimeAgo(post.created_at)"></p>
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-1 text-sm px-2 py-1 rounded-full"
:class="darkMode ? 'bg-red-500/20 text-red-400' : 'bg-red-50 text-red-500'">
<i class="fas fa-fire"></i>
<span>Trending</span>
</div>
</div>
</div>
<!-- Post Content -->
<div class="mb-4">
<p class="text-lg leading-relaxed"
:class="darkMode ? 'text-gray-100' : 'text-gray-800'"
x-text="post.content"></p>
</div>
<!-- Post Stats -->
<div class="flex items-center space-x-6 text-sm"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'">
<div class="flex items-center space-x-1">
<i class="fas fa-heart"></i>
<span x-text="formatNumber(post.like_count || 0)"></span>
</div>
<div class="flex items-center space-x-1">
<i class="far fa-comment"></i>
<span x-text="formatNumber(post.comment_count || 0)"></span>
</div>
<div class="flex items-center space-x-1">
<i class="fas fa-share"></i>
<span x-text="formatNumber(post.share_count || 0)"></span>
</div>
</div>
</article>
</template>
</div>
<!-- Empty State for Posts -->
<div x-show="!loading && posts.length === 0" class="text-center py-16">
<div class="mb-6">
<i class="fas fa-fire text-6xl mb-4"
:class="darkMode ? 'text-gray-600' : 'text-gray-300'"></i>
</div>
<h3 class="text-2xl font-bold mb-2"
:class="darkMode ? 'text-white' : 'text-gray-900'">
Keine beliebten Posts
</h3>
<p class="text-lg"
:class="darkMode ? 'text-gray-400' : 'text-gray-600'">
Noch keine Posts sind viral gegangen. Sei der Erste!
</p>
</div>
</div>
<!-- Trending Tab -->
<div x-show="activeTab === 'trending' && !loading">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<template x-for="item in trending" :key="item.id">
<div class="rounded-2xl p-6 shadow-lg border transition-all duration-300 hover:shadow-xl transform hover:scale-105"
:class="darkMode
? 'bg-gray-800/90 border-gray-700/50 backdrop-blur-xl'
: 'bg-white/80 border-gray-200/50 backdrop-blur-xl'">
<div class="flex items-center space-x-3 mb-4">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-orange-500 to-red-500 flex items-center justify-center text-white shadow-md">
<i class="fas fa-hashtag text-lg"></i>
</div>
<div class="flex-1">
<h3 class="font-bold text-lg"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="item.title"></h3>
<p class="text-sm"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'"
x-text="`${formatNumber(item.count)} Posts`"></p>
</div>
<div class="flex items-center space-x-1 text-sm px-2 py-1 rounded-full"
:class="darkMode ? 'bg-orange-500/20 text-orange-400' : 'bg-orange-50 text-orange-500'">
<i class="fas fa-trending-up"></i>
<span x-text="`+${item.growth}%`"></span>
</div>
</div>
<div x-show="item.description" class="mb-4">
<p class="text-sm"
:class="darkMode ? 'text-gray-300' : 'text-gray-700'"
x-text="item.description"></p>
</div>
<button class="w-full py-2 px-4 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
:class="darkMode
? 'bg-gray-700 text-white border border-gray-600 hover:bg-gray-600'
: 'bg-gray-100 text-gray-700 border border-gray-300 hover:bg-gray-200'">
Erkunden
</button>
</div>
</template>
</div>
<!-- Empty State for Trending -->
<div x-show="!loading && trending.length === 0" class="text-center py-16">
<div class="mb-6">
<i class="fas fa-trending-up text-6xl mb-4"
:class="darkMode ? 'text-gray-600' : 'text-gray-300'"></i>
</div>
<h3 class="text-2xl font-bold mb-2"
:class="darkMode ? 'text-white' : 'text-gray-900'">
Noch nichts im Trend
</h3>
<p class="text-lg"
:class="darkMode ? 'text-gray-400' : 'text-gray-600'">
Sei der Erste, der etwas Trending macht!
</p>
</div>
</div>
</div>
<!-- Quick Actions Floating Button -->
<div class="fixed bottom-6 right-6 z-40">
<div class="relative group">
<!-- Main FAB -->
<button class="w-14 h-14 rounded-full bg-gradient-to-r from-purple-500 to-indigo-500 text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-purple-500/50">
<i class="fas fa-plus text-xl"></i>
</button>
<!-- Action Menu -->
<div class="absolute bottom-16 right-0 opacity-0 group-hover:opacity-100 transition-all duration-300 transform scale-95 group-hover:scale-100 space-y-2">
<a href="{{ url_for('social_feed') }}"
class="flex items-center justify-center w-12 h-12 rounded-full bg-blue-500 text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-110"
title="Zum Feed">
<i class="fas fa-stream"></i>
</a>
<button class="flex items-center justify-center w-12 h-12 rounded-full bg-green-500 text-white shadow-lg hover:shadow-xl transition-all duration-300 transform hover:scale-110"
title="Post erstellen">
<i class="fas fa-edit"></i>
</button>
</div>
</div>
</div>
</div>
<script>
// Helper function for time formatting
function formatTimeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) return 'vor wenigen Sekunden';
if (diffInSeconds < 3600) return `vor ${Math.floor(diffInSeconds / 60)} Min`;
if (diffInSeconds < 86400) return `vor ${Math.floor(diffInSeconds / 3600)} Std`;
return `vor ${Math.floor(diffInSeconds / 86400)} Tagen`;
}
</script>
{% endblock %}

327
templates/social/feed.html Normal file
View File

@@ -0,0 +1,327 @@
{% extends "base.html" %}
{% block title %}Feed{% endblock %}
{% block content %}
<div class="min-h-screen"
x-data="{
posts: [],
loading: false,
page: 1,
hasMore: true,
newPostContent: '',
isPosting: false,
async loadPosts() {
if (this.loading || !this.hasMore) return;
this.loading = true;
try {
const response = await fetch(`/api/feed?page=${this.page}&per_page=10`);
const data = await response.json();
if (data.success) {
if (this.page === 1) {
this.posts = data.posts;
} else {
this.posts = [...this.posts, ...data.posts];
}
this.hasMore = data.has_next;
this.page++;
}
} catch (error) {
console.error('Error loading posts:', error);
} finally {
this.loading = false;
}
},
async createPost() {
if (!this.newPostContent.trim() || this.isPosting) return;
this.isPosting = true;
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: this.newPostContent
})
});
const data = await response.json();
if (data.success) {
this.posts.unshift(data.post);
this.newPostContent = '';
}
} catch (error) {
console.error('Error creating post:', error);
} finally {
this.isPosting = false;
}
},
async toggleLike(postId, index) {
try {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.posts[index].like_count = data.like_count;
this.posts[index].is_liked = data.is_liked;
}
} catch (error) {
console.error('Error toggling like:', error);
}
},
formatTimeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) return 'vor wenigen Sekunden';
if (diffInSeconds < 3600) return `vor ${Math.floor(diffInSeconds / 60)} Min`;
if (diffInSeconds < 86400) return `vor ${Math.floor(diffInSeconds / 3600)} Std`;
return `vor ${Math.floor(diffInSeconds / 86400)} Tagen`;
},
init() {
this.loadPosts();
}
}"
x-init="init()">
<!-- Main Container -->
<div class="max-w-2xl mx-auto px-4 py-6 space-y-6">
<!-- Post Composer -->
<div class="rounded-2xl p-6 shadow-lg border transition-all duration-300"
:class="darkMode
? 'bg-gray-800/90 border-gray-700/50 backdrop-blur-xl'
: 'bg-white/80 border-gray-200/50 backdrop-blur-xl'">
<!-- User Avatar and Input -->
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-lg shadow-md">
{{ current_user.username[0].upper() }}
</div>
</div>
<div class="flex-1">
<textarea
x-model="newPostContent"
@keydown.meta.enter="createPost()"
@keydown.ctrl.enter="createPost()"
class="w-full resize-none border-0 focus:ring-0 text-lg placeholder-gray-400 transition-all duration-200"
:class="darkMode
? 'bg-transparent text-white'
: 'bg-transparent text-gray-900'"
placeholder="Was beschäftigt dich heute?"
rows="3"></textarea>
</div>
</div>
<!-- Post Actions -->
<div class="mt-4 pt-4 border-t flex items-center justify-between"
:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
<div class="flex items-center space-x-4">
<button class="flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200"
:class="darkMode
? 'text-gray-300 hover:bg-gray-700/50'
: 'text-gray-500 hover:bg-gray-100'">
<i class="fas fa-image text-lg"></i>
<span class="text-sm font-medium">Foto</span>
</button>
<button class="flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200"
:class="darkMode
? 'text-gray-300 hover:bg-gray-700/50'
: 'text-gray-500 hover:bg-gray-100'">
<i class="fas fa-brain text-lg"></i>
<span class="text-sm font-medium">Gedanken</span>
</button>
</div>
<button @click="createPost()"
:disabled="!newPostContent.trim() || isPosting"
class="px-6 py-2.5 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-purple-500/50 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
:class="darkMode
? 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-lg hover:shadow-xl'
: 'bg-gradient-to-r from-purple-500 to-indigo-500 text-white shadow-md hover:shadow-lg'">
<span x-show="!isPosting">Teilen</span>
<span x-show="isPosting" class="flex items-center space-x-2">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Poste...</span>
</span>
</button>
</div>
</div>
<!-- Posts Feed -->
<div class="space-y-6">
<!-- Loading State -->
<div x-show="loading && posts.length === 0"
class="text-center py-12">
<div class="inline-flex items-center space-x-3"
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<svg class="animate-spin h-6 w-6" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-lg">Lade Posts...</span>
</div>
</div>
<!-- Empty State -->
<div x-show="!loading && posts.length === 0"
class="text-center py-16">
<div class="mb-6">
<i class="fas fa-stream text-6xl mb-4"
:class="darkMode ? 'text-gray-600' : 'text-gray-300'"></i>
</div>
<h3 class="text-2xl font-bold mb-2"
:class="darkMode ? 'text-white' : 'text-gray-900'">
Noch keine Posts
</h3>
<p class="text-lg mb-6"
:class="darkMode ? 'text-gray-400' : 'text-gray-600'">
Folge anderen Nutzern oder erstelle deinen ersten Post!
</p>
<a href="{{ url_for('discover') }}"
class="inline-flex items-center space-x-2 px-6 py-3 rounded-xl font-semibold transition-all duration-300 bg-gradient-to-r from-purple-500 to-indigo-500 text-white shadow-lg hover:shadow-xl transform hover:scale-105">
<i class="fas fa-compass"></i>
<span>Entdecken</span>
</a>
</div>
<!-- Posts -->
<template x-for="(post, index) in posts" :key="post.id">
<article class="rounded-2xl p-6 shadow-lg border transition-all duration-300 hover:shadow-xl"
:class="darkMode
? 'bg-gray-800/90 border-gray-700/50 backdrop-blur-xl'
: 'bg-white/80 border-gray-200/50 backdrop-blur-xl'">
<!-- Post Header -->
<div class="flex items-center space-x-3 mb-4">
<div class="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center text-white font-semibold text-lg shadow-md">
<span x-text="post.author.username.charAt(0).toUpperCase()"></span>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2">
<h4 class="font-semibold truncate"
:class="darkMode ? 'text-white' : 'text-gray-900'"
x-text="post.author.display_name || post.author.username"></h4>
<span x-show="post.author.is_verified"
class="text-blue-500">
<i class="fas fa-check-circle text-sm"></i>
</span>
</div>
<p class="text-sm"
:class="darkMode ? 'text-gray-400' : 'text-gray-500'"
x-text="formatTimeAgo(post.created_at)"></p>
</div>
<button class="p-2 rounded-lg transition-all duration-200"
:class="darkMode
? 'text-gray-400 hover:bg-gray-700/50'
: 'text-gray-400 hover:bg-gray-100'">
<i class="fas fa-ellipsis-h"></i>
</button>
</div>
<!-- Post Content -->
<div class="mb-4">
<p class="text-lg leading-relaxed whitespace-pre-wrap"
:class="darkMode ? 'text-gray-100' : 'text-gray-800'"
x-text="post.content"></p>
</div>
<!-- Post Actions -->
<div class="flex items-center justify-between pt-4 border-t"
:class="darkMode ? 'border-gray-700' : 'border-gray-200'">
<div class="flex items-center space-x-6">
<button @click="toggleLike(post.id, index)"
class="flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200 group"
:class="post.is_liked
? (darkMode ? 'text-red-400 bg-red-500/10' : 'text-red-500 bg-red-50')
: (darkMode ? 'text-gray-400 hover:bg-gray-700/50' : 'text-gray-500 hover:bg-gray-100')">
<i class="fas fa-heart transition-transform duration-200 group-hover:scale-110"
:class="post.is_liked ? 'fas' : 'far'"></i>
<span class="text-sm font-medium" x-text="post.like_count || 0"></span>
</button>
<button class="flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200"
:class="darkMode
? 'text-gray-400 hover:bg-gray-700/50'
: 'text-gray-500 hover:bg-gray-100'">
<i class="far fa-comment"></i>
<span class="text-sm font-medium" x-text="post.comment_count || 0"></span>
</button>
<button class="flex items-center space-x-2 px-3 py-2 rounded-lg transition-all duration-200"
:class="darkMode
? 'text-gray-400 hover:bg-gray-700/50'
: 'text-gray-500 hover:bg-gray-100'">
<i class="fas fa-share"></i>
<span class="text-sm font-medium" x-text="post.share_count || 0"></span>
</button>
</div>
<button class="p-2 rounded-lg transition-all duration-200"
:class="darkMode
? 'text-gray-400 hover:bg-gray-700/50'
: 'text-gray-400 hover:bg-gray-100'">
<i class="far fa-bookmark"></i>
</button>
</div>
</article>
</template>
</div>
<!-- Load More Button -->
<div x-show="hasMore && !loading && posts.length > 0"
class="text-center py-6">
<button @click="loadPosts()"
class="px-8 py-3 rounded-xl font-semibold transition-all duration-300 transform hover:scale-105"
:class="darkMode
? 'bg-gray-800 text-white border border-gray-700 hover:bg-gray-700'
: 'bg-white text-gray-700 border border-gray-200 hover:bg-gray-50'"
:disabled="loading">
<span x-show="!loading">Mehr laden</span>
<span x-show="loading" class="flex items-center space-x-2">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Lade...</span>
</span>
</button>
</div>
<!-- Loading More Posts -->
<div x-show="loading && posts.length > 0"
class="text-center py-6">
<div class="inline-flex items-center space-x-3"
:class="darkMode ? 'text-gray-300' : 'text-gray-600'">
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Lade weitere Posts...</span>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,381 @@
{% extends "base.html" %}
{% block title %}Benachrichtigungen - SysTades{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-6">
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">🔔 Benachrichtigungen</h1>
<button id="mark-all-read" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
Alle als gelesen markieren
</button>
</div>
</div>
<!-- Filter Tabs -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg mb-6">
<div class="border-b border-gray-200 dark:border-gray-600">
<nav class="flex space-x-8 px-6">
<button class="filter-btn active py-4 px-2 border-b-2 border-blue-500 font-medium text-blue-600 dark:text-blue-400" data-filter="all">
📥 Alle
</button>
<button class="filter-btn py-4 px-2 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" data-filter="unread">
🔴 Ungelesen
</button>
<button class="filter-btn py-4 px-2 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" data-filter="likes">
❤️ Likes
</button>
<button class="filter-btn py-4 px-2 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" data-filter="comments">
💬 Kommentare
</button>
<button class="filter-btn py-4 px-2 border-b-2 border-transparent font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200" data-filter="follows">
👥 Follows
</button>
</nav>
</div>
</div>
<!-- Notifications Container -->
<div id="notifications-container" class="space-y-4">
<!-- Notifications werden hier geladen -->
</div>
<!-- Load More Button -->
<div class="text-center mt-8">
<button id="load-more" class="px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors">
Mehr laden
</button>
</div>
</div>
<script>
class NotificationCenter {
constructor() {
this.currentFilter = 'all';
this.currentPage = 1;
this.isLoading = false;
this.hasMore = true;
this.initializeEventListeners();
this.loadNotifications();
}
initializeEventListeners() {
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const filter = e.target.dataset.filter;
this.switchFilter(filter);
});
});
// Mark all as read
document.getElementById('mark-all-read').addEventListener('click', () => {
this.markAllAsRead();
});
// Load more
document.getElementById('load-more').addEventListener('click', () => {
this.loadMoreNotifications();
});
}
switchFilter(filter) {
this.currentFilter = filter;
this.currentPage = 1;
this.hasMore = true;
// Update filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active', 'border-blue-500', 'text-blue-600', 'dark:text-blue-400');
btn.classList.add('border-transparent', 'text-gray-500', 'dark:text-gray-400');
});
const activeBtn = document.querySelector(`[data-filter="${filter}"]`);
activeBtn.classList.add('active', 'border-blue-500', 'text-blue-600', 'dark:text-blue-400');
activeBtn.classList.remove('border-transparent', 'text-gray-500', 'dark:text-gray-400');
this.loadNotifications();
}
async loadNotifications() {
if (this.isLoading) return;
this.isLoading = true;
try {
const params = new URLSearchParams({
page: this.currentPage,
per_page: 20,
filter: this.currentFilter
});
const response = await fetch(`/api/social/notifications?${params}`);
const result = await response.json();
if (result.success) {
if (this.currentPage === 1) {
document.getElementById('notifications-container').innerHTML = '';
}
this.renderNotifications(result.notifications);
this.hasMore = result.has_more;
this.updateLoadMoreButton();
} else {
this.showMessage('Fehler beim Laden der Benachrichtigungen', 'error');
}
} catch (error) {
console.error('Error loading notifications:', error);
this.showMessage('Fehler beim Laden der Benachrichtigungen', 'error');
} finally {
this.isLoading = false;
}
}
async loadMoreNotifications() {
if (!this.hasMore || this.isLoading) return;
this.currentPage++;
await this.loadNotifications();
}
renderNotifications(notifications) {
const container = document.getElementById('notifications-container');
if (notifications.length === 0 && this.currentPage === 1) {
container.innerHTML = `
<div class="text-center py-12">
<div class="text-6xl mb-4">📭</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Keine Benachrichtigungen</h3>
<p class="text-gray-600 dark:text-gray-300">
${this.currentFilter === 'unread' ? 'Alle Benachrichtigungen sind gelesen!' : 'Hier werden deine Benachrichtigungen angezeigt.'}
</p>
</div>
`;
return;
}
notifications.forEach(notification => {
const notificationElement = this.createNotificationElement(notification);
container.appendChild(notificationElement);
});
}
createNotificationElement(notification) {
const element = document.createElement('div');
element.className = `notification-item bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 ${
!notification.is_read ? 'border-l-4 border-blue-500' : ''
}`;
element.dataset.notificationId = notification.id;
const typeIcons = {
'like': '❤️',
'comment': '💬',
'follow': '👥',
'mention': '📢',
'system': '🔔'
};
element.innerHTML = `
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<div class="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white text-xl">
${typeIcons[notification.type] || '🔔'}
</div>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="text-gray-900 dark:text-white font-medium">
${notification.message}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
${this.formatDate(notification.created_at)}
</p>
</div>
<div class="flex items-center space-x-2 ml-4">
${!notification.is_read ? `
<button class="mark-read-btn px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
data-notification-id="${notification.id}">
Als gelesen markieren
</button>
` : ''}
<div class="relative">
<button class="notification-menu-btn p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
data-notification-id="${notification.id}">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="notification-menu hidden absolute right-0 mt-2 w-48 bg-white dark:bg-gray-700 rounded-md shadow-lg z-10">
<button class="delete-notification-btn block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600"
data-notification-id="${notification.id}">
Löschen
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
// Event listeners für die Buttons
const markReadBtn = element.querySelector('.mark-read-btn');
if (markReadBtn) {
markReadBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.markAsRead(notification.id);
});
}
const menuBtn = element.querySelector('.notification-menu-btn');
const menu = element.querySelector('.notification-menu');
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('hidden');
});
const deleteBtn = element.querySelector('.delete-notification-btn');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.deleteNotification(notification.id);
});
// Click outside to close menu
document.addEventListener('click', () => {
menu.classList.add('hidden');
});
return element;
}
async markAsRead(notificationId) {
try {
const response = await fetch(`/api/social/notifications/${notificationId}/read`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
const element = document.querySelector(`[data-notification-id="${notificationId}"]`);
element.classList.remove('border-l-4', 'border-blue-500');
const markReadBtn = element.querySelector('.mark-read-btn');
if (markReadBtn) {
markReadBtn.remove();
}
this.showMessage('Als gelesen markiert', 'success');
} else {
this.showMessage('Fehler beim Markieren', 'error');
}
} catch (error) {
console.error('Error marking as read:', error);
this.showMessage('Fehler beim Markieren', 'error');
}
}
async markAllAsRead() {
try {
const response = await fetch('/api/social/notifications/mark-all-read', {
method: 'POST'
});
const result = await response.json();
if (result.success) {
// Remove all unread indicators
document.querySelectorAll('.notification-item').forEach(item => {
item.classList.remove('border-l-4', 'border-blue-500');
const markReadBtn = item.querySelector('.mark-read-btn');
if (markReadBtn) {
markReadBtn.remove();
}
});
this.showMessage('Alle Benachrichtigungen als gelesen markiert', 'success');
} else {
this.showMessage('Fehler beim Markieren aller Benachrichtigungen', 'error');
}
} catch (error) {
console.error('Error marking all as read:', error);
this.showMessage('Fehler beim Markieren aller Benachrichtigungen', 'error');
}
}
async deleteNotification(notificationId) {
if (!confirm('Diese Benachrichtigung wirklich löschen?')) return;
try {
const response = await fetch(`/api/social/notifications/${notificationId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
const element = document.querySelector(`[data-notification-id="${notificationId}"]`);
element.remove();
this.showMessage('Benachrichtigung gelöscht', 'success');
} else {
this.showMessage('Fehler beim Löschen', 'error');
}
} catch (error) {
console.error('Error deleting notification:', error);
this.showMessage('Fehler beim Löschen', 'error');
}
}
updateLoadMoreButton() {
const loadMoreBtn = document.getElementById('load-more');
if (this.hasMore) {
loadMoreBtn.style.display = 'block';
loadMoreBtn.textContent = this.isLoading ? 'Lädt...' : 'Mehr laden';
loadMoreBtn.disabled = this.isLoading;
} else {
loadMoreBtn.style.display = 'none';
}
}
formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);
if (diffInSeconds < 60) return 'Gerade eben';
if (diffInSeconds < 3600) return `vor ${Math.floor(diffInSeconds / 60)} Min`;
if (diffInSeconds < 86400) return `vor ${Math.floor(diffInSeconds / 3600)} Std`;
if (diffInSeconds < 2592000) return `vor ${Math.floor(diffInSeconds / 86400)} Tagen`;
return date.toLocaleDateString('de-DE');
}
showMessage(message, type) {
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${
type === 'success' ? 'bg-green-500 text-white' :
type === 'error' ? 'bg-red-500 text-white' : 'bg-blue-500 text-white'
}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new NotificationCenter();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,668 @@
{% extends "base.html" %}
{% block title %}{{ user.display_name or user.username }} - SysTades{% endblock %}
{% block additional_css %}
<style>
.profile-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.profile-header {
background: white;
border-radius: 16px;
padding: 30px;
margin-bottom: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #e1e8ed;
position: relative;
overflow: hidden;
}
.profile-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 120px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
z-index: 1;
}
.profile-content {
position: relative;
z-index: 2;
}
.profile-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
font-weight: bold;
color: #667eea;
margin: 60px auto 20px auto;
border: 4px solid white;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.profile-info {
text-align: center;
color: white;
margin-bottom: 30px;
}
.profile-name {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
}
.profile-username {
font-size: 18px;
opacity: 0.9;
margin: 0 0 15px 0;
}
.profile-bio {
font-size: 16px;
line-height: 1.5;
opacity: 0.95;
max-width: 600px;
margin: 0 auto;
}
.profile-stats {
display: flex;
justify-content: center;
gap: 40px;
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
margin-top: 25px;
}
.stat-item {
text-align: center;
color: white;
}
.stat-number {
font-size: 24px;
font-weight: 700;
display: block;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
margin-top: 4px;
}
.profile-actions {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 25px;
}
.action-btn {
background: white;
color: #667eea;
border: none;
padding: 12px 24px;
border-radius: 25px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.action-btn:hover {
background: #f0f2f5;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.action-btn.primary {
background: #1877f2;
color: white;
}
.action-btn.primary:hover {
background: #166fe5;
}
.profile-navigation {
background: white;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #e1e8ed;
overflow: hidden;
}
.nav-tabs {
display: flex;
border-bottom: 1px solid #e1e8ed;
}
.nav-tab {
flex: 1;
background: none;
border: none;
padding: 15px 20px;
cursor: pointer;
font-weight: 500;
color: #8a8a8a;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.nav-tab:hover {
background: #f8f9fa;
color: #1d2129;
}
.nav-tab.active {
color: #1877f2;
border-bottom: 2px solid #1877f2;
background: #f0f2f5;
}
.profile-content-area {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border: 1px solid #e1e8ed;
min-height: 400px;
}
.posts-grid {
padding: 20px;
display: grid;
gap: 20px;
}
.post-card {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
border: 1px solid #e1e8ed;
transition: all 0.3s ease;
}
.post-card:hover {
background: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.post-meta {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
color: #8a8a8a;
font-size: 14px;
}
.post-content {
color: #1d2129;
line-height: 1.6;
margin-bottom: 15px;
}
.post-stats {
display: flex;
gap: 20px;
color: #8a8a8a;
font-size: 14px;
}
.post-stat {
display: flex;
align-items: center;
gap: 6px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #8a8a8a;
}
.empty-state h3 {
margin-bottom: 12px;
color: #1d2129;
}
.about-grid {
padding: 20px;
display: grid;
gap: 20px;
}
.about-section {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
border: 1px solid #e1e8ed;
}
.about-section h3 {
margin: 0 0 15px 0;
color: #1d2129;
font-size: 18px;
}
.info-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: #1d2129;
}
.info-row i {
color: #1877f2;
width: 20px;
}
/* Responsive */
@media (max-width: 768px) {
.profile-stats {
gap: 20px;
}
.profile-actions {
flex-direction: column;
align-items: center;
}
.nav-tabs {
overflow-x: auto;
}
}
/* Dark Mode */
[data-theme="dark"] .profile-header,
[data-theme="dark"] .profile-navigation,
[data-theme="dark"] .profile-content-area,
[data-theme="dark"] .about-section {
background: #242526;
border-color: #3a3b3c;
color: #e4e6ea;
}
[data-theme="dark"] .post-card {
background: #3a3b3c;
border-color: #4e4f50;
}
[data-theme="dark"] .post-card:hover {
background: #4e4f50;
}
[data-theme="dark"] .nav-tab {
color: #b0b3b8;
}
[data-theme="dark"] .nav-tab:hover {
background: #3a3b3c;
color: #e4e6ea;
}
[data-theme="dark"] .action-btn {
background: #3a3b3c;
color: #e4e6ea;
}
</style>
{% endblock %}
{% block content %}
<div class="profile-container">
<!-- Profile Header -->
<div class="profile-header">
<div class="profile-content">
<div class="profile-avatar">
{{ user.username[0].upper() }}
</div>
<div class="profile-info">
<h1 class="profile-name">{{ user.display_name or user.username }}</h1>
<p class="profile-username">@{{ user.username }}</p>
{% if user.bio %}
<p class="profile-bio">{{ user.bio }}</p>
{% endif %}
</div>
<div class="profile-stats">
<div class="stat-item">
<span class="stat-number">{{ user.post_count }}</span>
<div class="stat-label">Posts</div>
</div>
<div class="stat-item">
<span class="stat-number">{{ user.follower_count }}</span>
<div class="stat-label">Follower</div>
</div>
<div class="stat-item">
<span class="stat-number">{{ user.following_count }}</span>
<div class="stat-label">Folgt</div>
</div>
<div class="stat-item">
<span class="stat-number">{{ user.mindmaps|length if user.mindmaps else 0 }}</span>
<div class="stat-label">Mindmaps</div>
</div>
</div>
{% if user != current_user %}
<div class="profile-actions">
<button
class="action-btn primary"
onclick="followUser({{ user.id }})"
id="followBtn"
>
<i class="fas fa-user-plus"></i>
{% if is_following %}Gefolgt{% else %}Folgen{% endif %}
</button>
<button class="action-btn" onclick="sendMessage({{ user.id }})">
<i class="fas fa-envelope"></i>
Nachricht
</button>
</div>
{% else %}
<div class="profile-actions">
<a href="{{ url_for('settings') }}" class="action-btn">
<i class="fas fa-cog"></i>
Profil bearbeiten
</a>
<a href="{{ url_for('create_mindmap') }}" class="action-btn primary">
<i class="fas fa-plus"></i>
Neue Mindmap
</a>
</div>
{% endif %}
</div>
</div>
<!-- Profile Navigation -->
<div class="profile-navigation">
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('posts')">
<i class="fas fa-th-large"></i>
Posts
</button>
<button class="nav-tab" onclick="switchTab('mindmaps')">
<i class="fas fa-project-diagram"></i>
Mindmaps
</button>
<button class="nav-tab" onclick="switchTab('thoughts')">
<i class="fas fa-lightbulb"></i>
Gedanken
</button>
<button class="nav-tab" onclick="switchTab('about')">
<i class="fas fa-info-circle"></i>
Über
</button>
</div>
</div>
<!-- Profile Content Area -->
<div class="profile-content-area">
<!-- Posts Tab -->
<div id="posts-tab" class="tab-content">
<div class="posts-grid">
{% if posts %}
{% for post in posts %}
<div class="post-card">
<div class="post-meta">
<span><i class="fas fa-clock"></i> {{ post.created_at.strftime('%d.%m.%Y') }}</span>
<span><i class="fas fa-eye"></i> {{ post.view_count }} Aufrufe</span>
</div>
<div class="post-content">
{{ post.content[:300] }}{% if post.content|length > 300 %}...{% endif %}
</div>
<div class="post-stats">
<div class="post-stat">
<i class="fas fa-heart"></i>
<span>{{ post.like_count }}</span>
</div>
<div class="post-stat">
<i class="fas fa-comment"></i>
<span>{{ post.comment_count }}</span>
</div>
<div class="post-stat">
<i class="fas fa-share"></i>
<span>{{ post.share_count or 0 }}</span>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<h3>Noch keine Posts</h3>
<p>{% if user == current_user %}Du hast noch keine Posts erstellt.{% else %}{{ user.username }} hat noch keine Posts veröffentlicht.{% endif %}</p>
{% if user == current_user %}
<a href="{{ url_for('social_feed') }}" class="action-btn primary" style="margin-top: 15px;">
<i class="fas fa-plus"></i>
Ersten Post erstellen
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Mindmaps Tab -->
<div id="mindmaps-tab" class="tab-content" style="display: none;">
<div class="posts-grid">
{% if user.mindmaps %}
{% for mindmap in user.mindmaps %}
<div class="post-card">
<div class="post-meta">
<span><i class="fas fa-clock"></i> {{ mindmap.created_at.strftime('%d.%m.%Y') }}</span>
<span><i class="fas fa-nodes"></i> {{ mindmap.public_nodes|length }} Knoten</span>
</div>
<div class="post-content">
<h4 style="margin: 0 0 10px 0; color: #1877f2;">{{ mindmap.name }}</h4>
<p>{{ mindmap.description or 'Keine Beschreibung verfügbar' }}</p>
</div>
<div class="post-stats">
<a href="{{ url_for('user_mindmap', mindmap_id=mindmap.id) }}" class="action-btn">
<i class="fas fa-eye"></i>
Anzeigen
</a>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<h3>Keine Mindmaps</h3>
<p>{% if user == current_user %}Du hast noch keine Mindmaps erstellt.{% else %}{{ user.username }} hat noch keine öffentlichen Mindmaps.{% endif %}</p>
{% if user == current_user %}
<a href="{{ url_for('create_mindmap') }}" class="action-btn primary" style="margin-top: 15px;">
<i class="fas fa-plus"></i>
Erste Mindmap erstellen
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Thoughts Tab -->
<div id="thoughts-tab" class="tab-content" style="display: none;">
<div class="posts-grid">
{% if user.thoughts %}
{% for thought in user.thoughts[:10] %}
<div class="post-card">
<div class="post-meta">
<span><i class="fas fa-clock"></i> {{ thought.created_at.strftime('%d.%m.%Y') }}</span>
<span><i class="fas fa-tag"></i> {{ thought.branch }}</span>
</div>
<div class="post-content">
<h4 style="margin: 0 0 10px 0; color: #1877f2;">{{ thought.title }}</h4>
<p>{{ thought.abstract or thought.content[:200] }}{% if (thought.abstract or thought.content)|length > 200 %}...{% endif %}</p>
</div>
<div class="post-stats">
<div class="post-stat">
<i class="fas fa-star"></i>
<span>{{ thought.average_rating or 0 }}</span>
</div>
<div class="post-stat">
<i class="fas fa-comment"></i>
<span>{{ thought.comments|length }}</span>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<h3>Keine Gedanken</h3>
<p>{% if user == current_user %}Du hast noch keine Gedanken geteilt.{% else %}{{ user.username }} hat noch keine Gedanken veröffentlicht.{% endif %}</p>
</div>
{% endif %}
</div>
</div>
<!-- About Tab -->
<div id="about-tab" class="tab-content" style="display: none;">
<div class="about-grid">
<div class="about-section">
<h3>Grundlegende Informationen</h3>
<div class="info-row">
<i class="fas fa-user"></i>
<span>Mitglied seit {{ user.created_at.strftime('%B %Y') }}</span>
</div>
{% if user.location %}
<div class="info-row">
<i class="fas fa-map-marker-alt"></i>
<span>{{ user.location }}</span>
</div>
{% endif %}
{% if user.website %}
<div class="info-row">
<i class="fas fa-globe"></i>
<a href="{{ user.website }}" target="_blank" rel="noopener">{{ user.website }}</a>
</div>
{% endif %}
{% if user.last_login %}
<div class="info-row">
<i class="fas fa-clock"></i>
<span>Zuletzt aktiv: {{ user.last_login.strftime('%d.%m.%Y') }}</span>
</div>
{% endif %}
</div>
<div class="about-section">
<h3>Aktivitätsstatistiken</h3>
<div class="info-row">
<i class="fas fa-chart-line"></i>
<span>{{ user.post_count }} Posts erstellt</span>
</div>
<div class="info-row">
<i class="fas fa-lightbulb"></i>
<span>{{ user.thoughts|length if user.thoughts else 0 }} Gedanken geteilt</span>
</div>
<div class="info-row">
<i class="fas fa-project-diagram"></i>
<span>{{ user.mindmaps|length if user.mindmaps else 0 }} Mindmaps erstellt</span>
</div>
<div class="info-row">
<i class="fas fa-comments"></i>
<span>Aktives Community-Mitglied</span>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block additional_js %}
<script>
// Tab Navigation
function switchTab(tabName) {
// Alle Tabs verstecken
document.querySelectorAll('.tab-content').forEach(tab => {
tab.style.display = 'none';
});
// Alle Tab-Buttons deaktivieren
document.querySelectorAll('.nav-tab').forEach(btn => {
btn.classList.remove('active');
});
// Gewählten Tab anzeigen
document.getElementById(tabName + '-tab').style.display = 'block';
// Gewählten Tab-Button aktivieren
event.target.classList.add('active');
}
// Follow/Unfollow Funktionalität
async function followUser(userId) {
const button = document.getElementById('followBtn');
try {
const response = await fetch('/api/users/' + userId + '/follow', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const data = await response.json();
if (data.success) {
if (data.action === 'followed') {
button.innerHTML = '<i class="fas fa-user-check"></i> Gefolgt';
button.classList.add('following');
} else {
button.innerHTML = '<i class="fas fa-user-plus"></i> Folgen';
button.classList.remove('following');
}
} else {
alert(data.error && data.error.message ? data.error.message : 'Fehler beim Folgen');
}
} catch (error) {
console.error('Fehler beim Folgen:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
}
}
// Nachricht senden (Platzhalter)
function sendMessage(userId) {
alert('Nachrichten-Feature wird bald verfügbar sein!');
}
// URL-Parameter für Tab-Navigation
document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
const tab = urlParams.get('tab');
if (tab) {
// Tab aus URL aktivieren
const tabButton = document.querySelector('[onclick="switchTab(\'' + tab + '\')"]');
if (tabButton) {
tabButton.click();
}
}
});
</script>
{% endblock %}

79
templates/ueber_uns.html Normal file
View File

@@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block title %}Über uns{% 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">Über uns</h1>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Unsere Vision</h2>
<p class="mb-4">
Systades ist ein innovatives Projekt, das darauf abzielt, das Teilen und Vernetzen von Wissen und Gedanken zu revolutionieren. Unsere Plattform ermöglicht es Nutzern, ihre Ideen in interaktiven Mindmaps zu organisieren und mit anderen zu teilen, wodurch ein kollaboratives Netzwerk des Wissens entsteht.
</p>
<p class="mb-4">
Wir glauben daran, dass Wissen am wertvollsten ist, wenn es geteilt und vernetzt wird. Durch die Verbindung verschiedener Perspektiven und Denkansätze entstehen neue Erkenntnisse und Innovationen.
</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Das Team</h2>
<p class="mb-4">
Till Tomczak und Marwin Medczinski arbeiten gemeinsam daran, Systades kontinuierlich zu verbessern und weiterzuentwickeln.
</p>
<!-- Platz für Team-Mitglieder -->
<div class="team-members space-y-6">
<!-- Beispiel für ein Team-Mitglied (kann als Vorlage verwendet werden) -->
<!--
<div class="team-member p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h3 class="text-lg font-bold mb-2">[Name]</h3>
<p class="text-gray-600 dark:text-gray-300 mb-2">[Position/Rolle]</p>
<p class="text-sm">[Kurze Beschreibung oder Verantwortlichkeiten]</p>
</div>
-->
</div>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Unsere Mission</h2>
<p class="mb-4">
Wir setzen uns dafür ein, eine Plattform zu schaffen, die:
</p>
<ul class="list-disc list-inside space-y-2 mb-4">
<li>Intuitive Werkzeuge für die Organisation und Visualisierung von Wissen bereitstellt</li>
<li>Die Zusammenarbeit und den Austausch zwischen Nutzern fördert</li>
<li>Kreativität und innovative Denkansätze unterstützt</li>
<li>Einen sicheren und respektvollen Raum für intellektuellen Austausch bietet</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-xl font-bold mb-4">Technologie & Innovation</h2>
<p class="mb-4">
Systades nutzt modernste Technologien und innovative Ansätze, um eine optimale Nutzererfahrung zu gewährleisten. Unsere Plattform wird kontinuierlich weiterentwickelt, um neue Funktionen und Verbesserungen zu integrieren.
</p>
<p>
Wir legen besonderen Wert auf:
</p>
<ul class="list-disc list-inside space-y-2 mt-2">
<li>Intuitive Benutzeroberfläche</li>
<li>Hohe Performance und Zuverlässigkeit</li>
<li>Datensicherheit und Privatsphäre</li>
<li>Barrierefreiheit und Inklusivität</li>
</ul>
</section>
<section>
<h2 class="text-xl font-bold mb-4">Kontakt & Feedback</h2>
<p class="mb-4">
Wir freuen uns über Ihr Feedback und Ihre Ideen zur Verbesserung von Systades. Gemeinsam können wir die Plattform weiter optimieren und an die Bedürfnisse unserer Nutzer anpassen.
</p>
<p>
Kontaktieren Sie uns gerne für Fragen, Anregungen oder Kooperationsmöglichkeiten.
</p>
</section>
</div>
</div>
{% endblock %}

1579
templates/user_mindmap.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -29,8 +29,8 @@ __all__ = [
'delete_user',
'create_admin_user',
# Server management
'run_development_server',
# Server management (imported separately to avoid circular imports)
# 'run_development_server' - available in utils.server module
]
# Import remaining modules that might depend on app
@@ -38,4 +38,4 @@ 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
# Removed server import to prevent circular import - access via utils.server directly

Binary file not shown.

Binary file not shown.

38
utils/check_db.py Normal file
View File

@@ -0,0 +1,38 @@
import sqlite3
def check_mindmap_nodes():
try:
conn = sqlite3.connect('database/systades.db')
cursor = conn.cursor()
# Check if the table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='mind_map_node';")
table_exists = cursor.fetchone()
if not table_exists:
print("Die Tabelle 'mind_map_node' existiert nicht!")
return
# Check for the "Wissen" node
cursor.execute("SELECT * FROM mind_map_node WHERE name = 'Wissen';")
wissen_node = cursor.fetchone()
if wissen_node:
print(f"'Wissen'-Knoten gefunden: {wissen_node}")
else:
print("'Wissen'-Knoten NICHT gefunden!")
# Get all nodes
cursor.execute("SELECT id, name FROM mind_map_node LIMIT 10;")
nodes = cursor.fetchall()
print(f"\nVorhandene Knoten (max. 10):")
for node in nodes:
print(f" - {node}")
conn.close()
except Exception as e:
print(f"Fehler: {e}")
if __name__ == "__main__":
check_mindmap_nodes()

View File

@@ -1,17 +1,17 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import current_app
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import text
import time
def check_db_connection(db):
def check_db_connection(db, app=None):
"""
Überprüft die Datenbankverbindung und versucht ggf. die Verbindung wiederherzustellen
Args:
db: SQLAlchemy-Instanz
app: Flask-App-Instanz (optional, falls nicht im App-Kontext)
Returns:
bool: True, wenn die Verbindung erfolgreich ist, sonst False
@@ -22,7 +22,11 @@ def check_db_connection(db):
while retry_count < max_retries:
try:
# Führe eine einfache Abfrage durch, um die Verbindung zu testen
with current_app.app_context():
if app:
with app.app_context():
db.session.execute(text('SELECT 1'))
else:
# Versuche ohne expliziten App-Kontext (falls bereits im Kontext)
db.session.execute(text('SELECT 1'))
return True
except SQLAlchemyError as e:
@@ -38,42 +42,60 @@ def check_db_connection(db):
db.session.rollback()
except:
pass
except Exception as e:
print(f"Allgemeiner Fehler bei DB-Check: {str(e)}")
retry_count += 1
if retry_count < max_retries:
time.sleep(1)
return False
def initialize_db_if_needed(db, initialize_function=None):
def initialize_db_if_needed(db, initialize_function=None, app=None):
"""
Initialisiert die Datenbank, falls erforderlich
Args:
db: SQLAlchemy-Instanz
initialize_function: Funktion, die aufgerufen wird, um die Datenbank zu initialisieren
app: Flask-App-Instanz (optional, falls nicht im App-Kontext)
Returns:
bool: True, wenn die Datenbank bereit ist, sonst False
"""
# Prüfe die Verbindung
if not check_db_connection(db):
if not check_db_connection(db, app):
return False
# Prüfe, ob die Tabellen existieren
try:
with current_app.app_context():
# Führe eine Testabfrage auf einer Tabelle durch
if app:
with app.app_context():
# Führe eine Testabfrage auf einer Tabelle durch
db.session.execute(text('SELECT COUNT(*) FROM user'))
else:
# Versuche ohne expliziten App-Kontext
db.session.execute(text('SELECT COUNT(*) FROM user'))
except SQLAlchemyError:
# Tabellen existieren nicht, erstelle sie
try:
with current_app.app_context():
if app:
with app.app_context():
db.create_all()
# Rufe die Initialisierungsfunktion auf, falls vorhanden
if initialize_function and callable(initialize_function):
initialize_function()
else:
db.create_all()
# Rufe die Initialisierungsfunktion auf, falls vorhanden
if initialize_function and callable(initialize_function):
initialize_function()
return True
return True
except Exception as e:
print(f"Fehler bei DB-Initialisierung: {str(e)}")
return False
except Exception as e:
print(f"Fehler beim Prüfen der Datenbank-Tabellen: {str(e)}")
return False
return True

View File

@@ -11,19 +11,33 @@ import importlib.util
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
from app import app, db_path
# Direkt den Datenbankpfad berechnen, statt ihn aus app.py zu importieren
def get_db_path():
"""Berechnet den absoluten Pfad zur Datenbank"""
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
return os.path.join(basedir, 'database', 'systades.db')
# Import models direkt
from models import db
def ensure_db_dir():
"""Make sure the database directory exists."""
db_path = get_db_path()
os.makedirs(os.path.dirname(db_path), exist_ok=True)
def fix_database_schema():
"""Fix the database schema by adding missing columns."""
# Import Flask-App erst innerhalb der Funktion
from flask import Flask
from app import app
with app.app_context():
# Ensure directory exists
ensure_db_dir()
# Get database path
db_path = get_db_path()
# 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...")

Some files were not shown because too many files have changed in this diff Show More