Compare commits
176 Commits
a0e4cd2208
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f0d13cae78 | |||
| a60b5c31ca | |||
| f5c2e70a11 | |||
| 1f4394e9b6 | |||
| 37c457ca3f | |||
| 936d983cb3 | |||
| 9ed9adfeaf | |||
| 9c1475844c | |||
| 310d0af0d1 | |||
| cab8d28aeb | |||
| 9dc44f94f6 | |||
| 5b9ae85453 | |||
| 302d5213ef | |||
| bb3211ab3d | |||
| 2a246ee063 | |||
| fc8861c73c | |||
| 8c49e7396e | |||
| 8e3c81fd06 | |||
| f18d23cfea | |||
| d3405a7031 | |||
| 5793902e47 | |||
| e73ccd7e80 | |||
| e6784b712d | |||
| 35b5f321d4 | |||
| b68f65cc76 | |||
| 3a2f721f63 | |||
| 5933195196 | |||
| beccfa25a6 | |||
| bc5cef3ba8 | |||
| b867af9c8b | |||
| ee04432a49 | |||
| bbcee7f610 | |||
| 1eb47fc230 | |||
| 2921c5a824 | |||
| c98e238841 | |||
| af30a208ca | |||
| 2e2f35ccc1 | |||
| fd293e53e1 | |||
| 2b19cb000b | |||
| 3aefe6c5e6 | |||
| c7b87dc643 | |||
| dc96252013 | |||
| ab56f44ae9 | |||
| 61124f5266 | |||
| fab8d10f03 | |||
| dec30e4681 | |||
| a1bd999c6a | |||
| b1d33ce643 | |||
| 293f877017 | |||
| e86d0b0f90 | |||
| 059fd167d6 | |||
| 256d38e140 | |||
| 4b75489631 | |||
| cb95c78276 | |||
| 00cb100467 | |||
| 8c66461dc8 | |||
| 566f84fc0c | |||
| 07eae42ba3 | |||
| 0a1bebd862 | |||
| 59b79b3466 | |||
| 6f5526b648 | |||
| 21148f0c0e | |||
| ba6cac32a9 | |||
| be767e9f27 | |||
| 6aaf073ffb | |||
| b6080f96cf | |||
| 9ebf4b7abd | |||
| 5d35983f15 | |||
| 7278ece2b8 | |||
| f677e98795 | |||
| 40c3f6d9b4 | |||
| 9939db731b | |||
| d0f32a8355 | |||
| 02d1801fc9 | |||
| c51a8e23ca | |||
| 1600647bc4 | |||
| 82d03f6c48 | |||
| d1352286b7 | |||
| e7b3374c53 | |||
| 4bf046c657 | |||
| 892a1212d9 | |||
| 8440b7c30d | |||
| 74c2783b1a | |||
| fcd82eb5c9 | |||
| c654986f65 | |||
| f4ab617c59 | |||
| 9c36179f29 | |||
| f292cf1ce5 | |||
| 3a20ea0282 | |||
| 44986bfa23 | |||
| 41195a44cb | |||
| e1cd23230d | |||
| 77095e91b6 | |||
| 6322e046c5 | |||
| 2584bae149 | |||
| c0bd7a3986 | |||
| dec4a57b89 | |||
| 6a3b3a81c1 | |||
| 629813c486 | |||
| 7cb2bf1ed0 | |||
| ed1d41d316 | |||
| fe3cf81bc7 | |||
| 2e68ae30b8 | |||
| 858fdf5c44 | |||
| 4948f3ad2a | |||
| 52954e51f1 | |||
| 14f1356551 | |||
| 44c7183e97 | |||
| d99cae4956 | |||
| 3ae5f2527c | |||
| 412dabd5c1 | |||
| 5ade301f80 | |||
| 118f8ed132 | |||
| 121f46df01 | |||
| 4b0613eb6b | |||
| dd172d8596 | |||
| 653b3abe91 | |||
| ec50886145 | |||
| c888dcc452 | |||
| acceec4352 | |||
| f093a6211c | |||
| 58a5ea00bd | |||
| aeb829e36a | |||
| 49e5e19b7c | |||
| 903e095b66 | |||
| 2d083f5c0a | |||
| cbe8dc3bd0 | |||
| 7c1533c20d | |||
| c285b7d8dc | |||
| 21ddd38e13 | |||
| 1cf7bfbf76 | |||
| 40b28134fc | |||
| d5fababd49 | |||
| 7c742debdf | |||
| 4a4271a23c | |||
| c1038b479f | |||
| cd0083544a | |||
| a03bec2dff | |||
| 997479581d | |||
| 8153390e35 | |||
| bfa155628e | |||
| 700a8a3b89 | |||
| 808481ffe7 | |||
| e2c8cfaacf | |||
| 78e37fa717 | |||
| b2cf50626a | |||
| 7f48526315 | |||
| 84f8a6bf31 | |||
| 7003c89447 | |||
| d0821db983 | |||
| f0c4c514c4 | |||
| 304a399b85 | |||
| a5396c0d6e | |||
| 9cc4e70cba | |||
| a8cac08d30 | |||
| 42a7485ce1 | |||
| 54a5ccc224 | |||
| a99f82d4cf | |||
| 699127f41f | |||
| e8d356a27a | |||
| daf2704253 | |||
| 084059449f | |||
| c9bbc6ff25 | |||
| 742e3fda20 | |||
| 54aa246b79 | |||
| 505fb9aa47 | |||
| e4e6541b8c | |||
| e724181915 | |||
| 460c3f987e | |||
| 7f33dea278 | |||
| 726d9c9c70 | |||
| 81170fbd3d | |||
| eff3fda1ca | |||
| d49b266d96 | |||
| 34a08c4a6a | |||
| 7918de1723 |
4
.env
4
.env
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
FLASK_APP=app.py
|
FLASK_APP=app.py
|
||||||
FLASK_DEBUG=1
|
FLASK_ENV=development
|
||||||
SECRET_KEY=your-secret-key-replace-in-production
|
SECRET_KEY=your-secret-key-replace-in-production
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
OPENAI_API_KEY=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
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
|
|||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
logs/app.log
|
||||||
1035
COMMON_ERRORS.md
1035
COMMON_ERRORS.md
File diff suppressed because it is too large
Load Diff
552
ROADMAP.md
552
ROADMAP.md
@@ -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
|
### Phase 1: Basis Social Network ✅
|
||||||
- [x] Implementierung der Modelle in models.py
|
- ✅ Erweiterte Benutzermodelle mit Social Features
|
||||||
- [x] Erstellung der API-Endpunkte für CRUD-Operationen
|
- ✅ Posts, Kommentare, Likes, Follows System
|
||||||
- [x] Integration mit der bestehenden Benutzerauthentifizierung
|
- ✅ Benachrichtigungssystem
|
||||||
- [x] Seed-Daten für die Entwicklung und Tests
|
- ✅ 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
|
### Phase 3: Erweiterte Social Features ✅
|
||||||
- [x] Implementierung von AJAX-Anfragen zum Laden der Mindmap-Daten
|
- ✅ Benutzerprofile mit Tabs (Posts, Gedanken, Mindmaps, Aktivität)
|
||||||
- [x] Dynamisches Rendering der Knoten, Verbindungen und Labels
|
- ✅ Follow/Unfollow System mit UI
|
||||||
- [x] Drag-and-Drop-Funktionalität für die Bewegung von Knoten
|
- ✅ Notification Center mit Filtering
|
||||||
- [x] Zoom- und Pan-Funktionalität mit Persistenz der Ansicht
|
- ✅ Post-Typen (Text, Gedanke, Frage, Erkenntnis)
|
||||||
- [x] Verbesserte Fehlerbehandlung in der Knotenvisualisierung
|
- ✅ Sichtbarkeitseinstellungen (Öffentlich, Follower, Privat)
|
||||||
- [x] Robustere Verbindungserkennung zwischen Knoten
|
- ✅ Quick-Create Post Modal
|
||||||
- [x] Implementierung von Glasmorphismus-Effekten für moderneres UI
|
|
||||||
|
|
||||||
## 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
|
## 🔄 Aktuelle Phase 4: UI/UX Verbesserungen (In Arbeit)
|
||||||
- [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
|
|
||||||
|
|
||||||
## 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
|
### Performance Optimierungen
|
||||||
- [ ] UI für das Erstellen und Bearbeiten eigener Mindmaps
|
- ⏳ Lazy Loading für Posts
|
||||||
- [ ] Funktion zum Hinzufügen/Entfernen von Knoten aus der öffentlichen Mindmap
|
- ⏳ Image Optimization
|
||||||
- [ ] Speichern der Knotenpositionen und Ansichtseinstellungen
|
- ⏳ Caching System
|
||||||
- [ ] Benutzerspezifische Visualisierungseinstellungen
|
- ⏳ API Rate Limiting
|
||||||
- [ ] Dashboard mit Übersicht aller Mindmaps des Benutzers
|
- ⏳ Database Indexing
|
||||||
|
|
||||||
## Phase 5: Notizen und Annotationen
|
## 📈 Kommende Phasen
|
||||||
|
|
||||||
- [x] Anzeige von Gedanken zu Mindmap-Knoten
|
### Phase 5: Community Features
|
||||||
- [ ] UI für das Hinzufügen privater Notizen zu Knoten
|
- 🔲 Gruppen/Communities System
|
||||||
- [ ] Visuelle Anzeige von Notizen in der Mindmap
|
- 🔲 Events und Kalenderfunktion
|
||||||
- [ ] Texteditor mit Markdown-Unterstützung für Notizen
|
- 🔲 Live Discussions/Chats
|
||||||
- [ ] Kategorisierung und Farbkodierung von Notizen
|
- 🔲 Trending Topics/Hashtags
|
||||||
- [ ] Suchfunktion für Notizen
|
- 🔲 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
|
### Phase 7: Monetarisierung & Skalierung
|
||||||
- [ ] Verknüpfen von Quellen mit Mindmap-Knoten
|
- 🔲 Premium Features
|
||||||
- [ ] Upload-Funktionalität für Dateien und Medien
|
- 🔲 Creator Economy Tools
|
||||||
- [ ] Verwaltung von Zitaten und Referenzen
|
- 🔲 API für Drittanbieter
|
||||||
- [ ] Visuelles Feedback für Tags und Quellen in der Mindmap
|
- 🔲 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)
|
## 🏗️ Technische Architektur
|
||||||
- [ ] Teilen von Mindmaps (öffentlich/privat/mit bestimmten Benutzern)
|
|
||||||
- [ ] Kollaborative Bearbeitung von Mindmaps
|
|
||||||
- [ ] Verknüpfung mit externen Ressourcen (Links, Dateien)
|
|
||||||
- [ ] Versionierung von Mindmaps
|
|
||||||
|
|
||||||
## 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
|
### Frontend Stack ✅
|
||||||
- [ ] Automatische Kategorisierung von Inhalten
|
- **Styling**: TailwindCSS mit Custom Themes
|
||||||
- [ ] Visualisierung von Beziehungsstärken und -typen
|
- **JavaScript**: Vanilla JS mit ES6+ Features
|
||||||
- [ ] Mindmap-Statistiken und Analysen
|
- **Icons**: Font Awesome 6
|
||||||
- [ ] KI-basierte Zusammenfassung von Teilbereichen der Mindmap
|
- **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
|
-- Relationship Tables
|
||||||
- [ ] Verbesserung der Benutzerfreundlichkeit basierend auf Feedback
|
user_friendships (Freundschaftssystem)
|
||||||
- [ ] Erweiterte Such- und Filterfunktionen
|
user_follows (Follow System)
|
||||||
- [ ] Mobile Optimierung
|
post_likes (Like System)
|
||||||
- [ ] Offline-Funktionalität mit Synchronisierung
|
comment_likes (Comment Likes)
|
||||||
|
user_thought_bookmark (Bookmark System)
|
||||||
|
```
|
||||||
|
|
||||||
## Technische Schulden und Refactoring
|
## 📊 API Endpunkte
|
||||||
|
|
||||||
- [ ] Trennung der Datenbank-Logik vom Flask-App-Code
|
### Social Feed APIs ✅
|
||||||
- [ ] Einführung von Unit-Tests und Integration-Tests
|
- `GET /api/social/posts` - Feed Posts abrufen
|
||||||
- [ ] Überarbeitung der API-Dokumentation
|
- `POST /api/social/posts` - Neuen Post erstellen
|
||||||
- [ ] Caching-Strategien für bessere Performance
|
- `POST /api/social/posts/{id}/like` - Post liken/unliken
|
||||||
- [ ] Verbesserte Fehlerbehandlung und Logging
|
- `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
|
### Notification APIs ✅
|
||||||
- Integration von OpenAI mit dem gpt-4o-mini-Modell für den KI-Assistenten
|
- `GET /api/social/notifications` - Benachrichtigungen abrufen
|
||||||
- Datenbankzugriff für den KI-Assistenten, um direkt Informationen aus der Datenbank abzufragen
|
- `POST /api/social/notifications/{id}/read` - Als gelesen markieren
|
||||||
- Verbesserte Benutzeroberfläche für den KI-Assistenten mit kontextbezogenen Vorschlägen
|
- `POST /api/social/notifications/mark-all-read` - Alle als gelesen
|
||||||
|
- `DELETE /api/social/notifications/{id}` - Benachrichtigung löschen
|
||||||
|
|
||||||
### Zukünftige Verbesserungen
|
### Analytics APIs ✅
|
||||||
- Implementierung von Vektorsuche für präzisere Datenbank-Abfragen durch die KI
|
- `GET /api/social/analytics/dashboard` - Benutzer-Analytics
|
||||||
- Erweiterung der KI-Funktionalität für tiefere Analyse von Zusammenhängen zwischen Gedanken
|
- `GET /api/social/bookmarks` - Gebookmarkte Posts
|
||||||
- KI-gestützte Vorschläge für neue Verbindungen zwischen Gedanken basierend auf Inhaltsanalyse
|
|
||||||
- Finetuning des KI-Modells auf die spezifischen Anforderungen der Anwendung
|
## 🔒 Sicherheit & Datenschutz
|
||||||
- Erweiterung auf multimodale Fähigkeiten (Bild- und Textanalyse)
|
|
||||||
|
### 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
|
### Niedrige Priorität
|
||||||
2. **MindMapNode** - Öffentliche Mindmap-Knoten mit Metadaten
|
1. 🔲 Email Benachrichtigungen
|
||||||
3. **UserMindmap** - Benutzerdefinierte Mindmaps
|
2. 🔲 Export/Import Features
|
||||||
4. **UserMindmapNode** - Verknüpfung zwischen Benutzermindmaps und öffentlichen Knoten
|
3. 🔲 Advanced Search Filters
|
||||||
5. **MindmapNote** - Benutzerspezifische Notizen
|
4. 🔲 Theming System
|
||||||
6. **Thought** - Gedanken und Inhalte, die Knoten zugeordnet sind
|
|
||||||
7. **ThoughtRelation** - Beziehungen zwischen Gedanken
|
|
||||||
|
|
||||||
### Frontend-Technologien
|
---
|
||||||
|
|
||||||
- D3.js für die Visualisierung der Mindmap
|
**Letzte Aktualisierung**: {{ current_date }}
|
||||||
- WebGL für den neuronalen Netzwerk-Hintergrund
|
**Version**: 2.0.0 - Social Network Release
|
||||||
- AJAX für dynamisches Laden von Daten
|
**Status**: ✅ Fully Functional Social Platform
|
||||||
- Interaktive Bedienelemente mit JavaScript
|
|
||||||
- Responsive Design mit Tailwind CSS
|
|
||||||
|
|
||||||
### Backend-APIs
|
# 🗺️ SysTades Roadmap
|
||||||
|
|
||||||
Die implementierten API-Endpunkte umfassen:
|
## ✅ Abgeschlossen (v1.0 - v1.3)
|
||||||
|
|
||||||
- `/api/mindmap/public` - Abrufen der öffentlichen Mindmap-Struktur
|
### 🎯 Grundfunktionen
|
||||||
- `/api/mindmap/user/<id>` - Abrufen benutzerdefinierter Mindmaps
|
- [x] **Benutzerauthentifizierung** - Registrierung, Login, Logout
|
||||||
- `/api/mindmap/<id>/add_node` - Hinzufügen eines Knotens zur Benutzer-Mindmap
|
- [x] **Interaktive Mindmap** - Cytoscape.js-basierte Visualisierung
|
||||||
- `/api/mindmap/<id>/remove_node/<node_id>` - Entfernen eines Knotens
|
- [x] **Gedankenverwaltung** - CRUD-Operationen für Thoughts
|
||||||
- `/api/mindmap/<id>/update_node_position` - Aktualisierung von Knotenpositionen
|
- [x] **Kategoriesystem** - Hierarchische Wissensorganisation
|
||||||
- `/api/mindmap/<id>/notes` - Verwaltung von Notizen
|
- [x] **Responsive Design** - Mobile-first Ansatz
|
||||||
- `/api/nodes/<id>/thoughts` - Abrufen und Hinzufügen von Gedanken zu Knoten
|
- [x] **Dark/Light Mode** - Benutzerfreundliche Themes
|
||||||
- `/api/get_dark_mode` - Abrufen der Dark Mode Einstellung
|
|
||||||
|
|
||||||
## 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
|
## 🚀 Neu implementiert (v1.4 - Social Network Update)
|
||||||
- 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
|
|
||||||
|
|
||||||
## Aktuelle Verbesserungen
|
### 📱 Social Network Features
|
||||||
- Tailwind CSS wurde auf CDN-Version aktualisiert (06.06.2024)
|
- [x] **Social Feed** - Instagram/Twitter-ähnlicher Feed
|
||||||
- Content Security Policy (CSP) für Tailwind CSS CDN und WebGL konfiguriert
|
- [x] **Post-System** - Erstellen, Liken, Kommentieren von Posts
|
||||||
- Behebung kritischer Fehler in der Mindmap-Knotenvisualisierung (15.06.2024)
|
- [x] **Follow-System** - Benutzer folgen und entfolgen
|
||||||
- Verbesserte Verbindungserkennung zwischen Knoten implementiert
|
- [x] **Discover-Seite** - Trending Posts und empfohlene Benutzer
|
||||||
- Robuste Fehlerbehandlung für verschiedene API-Datenformate
|
- [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)
|
### 🧠 Erweiterte Mindmap-Features
|
||||||
- Implementierung des Tagging-Systems für Gedanken
|
- [x] **Kollaborative Bearbeitung** - Vorbereitung für Echtzeit-Kollaboration
|
||||||
- Quellenmanagement für Mindmap-Knoten
|
- [x] **Mindmap-Export** - JSON-Export mit geplanten weiteren Formaten
|
||||||
- Erweiterte Benutzerprofilfunktionen
|
- [x] **Mindmap-Sharing** - Teilen von Mindmaps in sozialen Netzwerken
|
||||||
- Verbesserung der mobilen Benutzererfahrung
|
- [x] **Erweiterte Toolbar** - Neue Bearbeitungsoptionen
|
||||||
- Integration von Exportfunktionen für Mindmaps
|
- [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)
|
## 🔄 In Entwicklung (v1.5)
|
||||||
- Die flask-cors-Bibliothek und alle zugehörigen Initialisierungen wurden entfernt.
|
|
||||||
- CORS wird nicht mehr unterstützt oder benötigt.
|
### 🔄 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.
2526
app.py.bak
Normal file
2526
app.py.bak
Normal file
File diff suppressed because it is too large
Load Diff
5
cookies.txt
Normal file
5
cookies.txt
Normal 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.
@@ -4,12 +4,12 @@
|
|||||||
# Flask
|
# Flask
|
||||||
FLASK_APP=app.py
|
FLASK_APP=app.py
|
||||||
FLASK_ENV=development
|
FLASK_ENV=development
|
||||||
SECRET_KEY=your-secret-key-replace-in-production
|
SECRET_KEY=mein-sicherer-schluessel-fuer-entwicklung
|
||||||
|
|
||||||
# OpenAI API
|
# OpenAI API
|
||||||
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
# Bei Bedarf kann hier eine andere Datenbank-URL angegeben werden
|
||||||
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
# Der Pfad wird relativ zum Projektverzeichnis angegeben
|
||||||
# SQLALCHEMY_DATABASE_URI=sqlite:////absoluter/pfad/zu/database/systades.db
|
SQLALCHEMY_DATABASE_URI=sqlite:///database/systades.db
|
||||||
130
init_db.py
130
init_db.py
@@ -1,19 +1,29 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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 os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from datetime import datetime
|
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
|
# Erstelle eine temporäre Flask-App, um die Datenbank zu initialisieren
|
||||||
app = Flask(__name__)
|
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
|
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)
|
db.init_app(app)
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
@@ -69,45 +79,111 @@ def create_default_users():
|
|||||||
|
|
||||||
def create_default_categories():
|
def create_default_categories():
|
||||||
"""Erstellt die Standardkategorien für die Mindmap"""
|
"""Erstellt die Standardkategorien für die Mindmap"""
|
||||||
categories = [
|
# Hauptkategorien
|
||||||
|
main_categories = [
|
||||||
{
|
{
|
||||||
'name': 'Konzept',
|
"name": "Philosophie",
|
||||||
'description': 'Abstrakte Ideen und theoretische Konzepte',
|
"description": "Philosophisches Denken und Konzepte",
|
||||||
'color_code': '#6366f1',
|
"color_code": "#9F7AEA",
|
||||||
'icon': 'lightbulb'
|
"icon": "fa-brain"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Technologie',
|
"name": "Wissenschaft",
|
||||||
'description': 'Hardware, Software, Tools und Plattformen',
|
"description": "Wissenschaftliche Disziplinen und Erkenntnisse",
|
||||||
'color_code': '#10b981',
|
"color_code": "#60A5FA",
|
||||||
'icon': 'cpu'
|
"icon": "fa-flask"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Prozess',
|
"name": "Technologie",
|
||||||
'description': 'Workflows, Methodologien und Vorgehensweisen',
|
"description": "Technologische Entwicklungen und Anwendungen",
|
||||||
'color_code': '#f59e0b',
|
"color_code": "#10B981",
|
||||||
'icon': 'git-branch'
|
"icon": "fa-microchip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Person',
|
"name": "Künste",
|
||||||
'description': 'Personen, Teams und Organisationen',
|
"description": "Künstlerische Ausdrucksformen und Werke",
|
||||||
'color_code': '#ec4899',
|
"color_code": "#F59E0B",
|
||||||
'icon': 'user'
|
"icon": "fa-palette"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Dokument',
|
"name": "Psychologie",
|
||||||
'description': 'Dokumentationen, Referenzen und Ressourcen',
|
"description": "Mentale Prozesse und Verhaltensweisen",
|
||||||
'color_code': '#3b82f6',
|
"color_code": "#EF4444",
|
||||||
'icon': 'file-text'
|
"icon": "fa-brain"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
for cat_data in categories:
|
# Hauptkategorien erstellen
|
||||||
|
category_map = {}
|
||||||
|
for cat_data in main_categories:
|
||||||
category = Category(**cat_data)
|
category = Category(**cat_data)
|
||||||
db.session.add(category)
|
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()
|
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():
|
def create_sample_mindmap():
|
||||||
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
"""Erstellt eine Beispiel-Mindmap mit Knoten und Beziehungen"""
|
||||||
|
|||||||
0
instance/logs/app.log
Normal file
0
instance/logs/app.log
Normal file
0
instance/logs/errors.log
Normal file
0
instance/logs/errors.log
Normal file
0
instance/logs/social.log
Normal file
0
instance/logs/social.log
Normal file
0
logs/api.log
Normal file
0
logs/api.log
Normal file
2419
logs/app.log
Normal file
2419
logs/app.log
Normal file
File diff suppressed because it is too large
Load Diff
424
logs/errors.log
Normal file
424
logs/errors.log
Normal 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
|
||||||
|
|
||||||
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
38
migrations/versions/add_mindmap_shares_table.py
Normal file
38
migrations/versions/add_mindmap_shares_table.py
Normal 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')
|
||||||
40
migrations/versions/add_missing_user_fields.py
Normal file
40
migrations/versions/add_missing_user_fields.py
Normal 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 ###
|
||||||
352
models.py
352
models.py
@@ -45,6 +45,35 @@ user_thought_bookmark = db.Table('user_thought_bookmark',
|
|||||||
db.Column('created_at', db.DateTime, default=datetime.utcnow)
|
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):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
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)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
is_active = db.Column(db.Boolean, default=True)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
role = db.Column(db.String(20), default="user") # 'user', 'admin', 'moderator'
|
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)
|
threads = db.relationship('Thread', backref='creator', lazy=True)
|
||||||
messages = db.relationship('Message', backref='author', lazy=True)
|
messages = db.relationship('Message', backref='author', lazy=True)
|
||||||
projects = db.relationship('Project', backref='owner', 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):
|
def __repr__(self):
|
||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
@@ -68,6 +150,53 @@ class User(db.Model, UserMixin):
|
|||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password, 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):
|
class Category(db.Model):
|
||||||
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
"""Wissenschaftliche Kategorien für die Gliederung der öffentlichen Mindmap"""
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -343,3 +472,224 @@ class ForumPost(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<ForumPost {self.title}>'
|
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}>'
|
||||||
22
server.log
Normal file
22
server.log
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[2m⏰ 21:58:48.486[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet
|
||||||
|
[2m⏰ 21:58:48.486[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
|
||||||
|
[2m⏰ 21:58:49.951[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 OpenAI API-Verbindung erfolgreich hergestellt
|
||||||
|
[2m⏰ 21:58:50.122[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbank erfolgreich initialisiert
|
||||||
|
[2m⏰ 21:58:50.132[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbanktabellen erstellt/aktualisiert
|
||||||
|
[2m⏰ 21:58:50.134[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 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)
|
||||||
|
[2m⏰ 21:58:52.225[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet
|
||||||
|
[2m⏰ 21:58:52.226[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 🚀 SysTades Social Network gestartet (v1.0.0) in development Umgebung auf Port 5000
|
||||||
|
[2m⏰ 21:58:53.848[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 OpenAI API-Verbindung erfolgreich hergestellt
|
||||||
|
[2m⏰ 21:58:53.997[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbank erfolgreich initialisiert
|
||||||
|
[2m⏰ 21:58:54.002[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [35m🗄️ [DB ][0m [2m│[0m 🚫 Datenbanktabellen erstellt/aktualisiert
|
||||||
|
[2m⏰ 21:58:54.006[0m [2m│[0m [92m✅ INFO [0m [2m│[0m [33m⚙️ [SYSTEM ][0m [2m│[0m 📝 Starte Flask-Entwicklungsserver auf http://localhost:5000
|
||||||
|
* Debugger is active!
|
||||||
|
* Debugger PIN: 114-005-893
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
/* ChatGPT Assistent Styles - Verbesserte Version */
|
/* ChatGPT Assistent Styles - Verbesserte Version */
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
bottom: 5.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 85vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
@@ -10,6 +13,15 @@
|
|||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: calc(100vw - 2rem);
|
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 {
|
#assistant-toggle {
|
||||||
@@ -22,11 +34,6 @@
|
|||||||
transform: scale(1.1) rotate(10deg);
|
transform: scale(1.1) rotate(10deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#assistant-history {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
#assistant-history::-webkit-scrollbar {
|
#assistant-history::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
@@ -142,14 +149,21 @@
|
|||||||
.typing-indicator span {
|
.typing-indicator span {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
background-color: #888;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
opacity: 0.4;
|
opacity: 0.6;
|
||||||
animation: bounce 1.4s infinite ease-in-out;
|
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(1) { animation-delay: 0s; }
|
||||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||||
@@ -173,11 +187,12 @@
|
|||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
#assistant-chat {
|
#assistant-chat {
|
||||||
width: calc(100vw - 2rem) !important;
|
width: calc(100vw - 2rem) !important;
|
||||||
|
max-height: 65vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chatgpt-assistant {
|
#chatgpt-assistant {
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
bottom: 1rem;
|
bottom: 6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,3 +216,37 @@ main {
|
|||||||
footer {
|
footer {
|
||||||
flex-shrink: 0;
|
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;
|
||||||
|
}
|
||||||
@@ -68,18 +68,37 @@ body {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode */
|
/* Strikte Trennung: Dark Mode */
|
||||||
html.dark body {
|
html.dark body,
|
||||||
|
body.dark {
|
||||||
background-color: var(--bg-primary-dark);
|
background-color: var(--bg-primary-dark);
|
||||||
color: var(--text-primary-dark);
|
color: var(--text-primary-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode */
|
/* Strikte Trennung: Light Mode */
|
||||||
|
html:not(.dark) body,
|
||||||
body:not(.dark) {
|
body:not(.dark) {
|
||||||
background-color: var(--light-bg);
|
background-color: var(--light-bg);
|
||||||
color: var(--light-text);
|
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 */
|
/* Typography */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -388,7 +407,7 @@ html.dark ::-webkit-scrollbar-thumb:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
font-size: 1.5rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,22 +474,60 @@ body:not(.dark) a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Buttons */
|
/* 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) .btn,
|
||||||
body:not(.dark) button:not(.toggle) {
|
body:not(.dark) .btn-primary,
|
||||||
background-color: var(--light-primary);
|
body:not(.dark) .btn-secondary,
|
||||||
color: white;
|
body:not(.dark) .btn-success,
|
||||||
border: none;
|
body:not(.dark) .btn-danger,
|
||||||
box-shadow: var(--light-shadow);
|
body:not(.dark) .btn-warning,
|
||||||
border-radius: 0.375rem;
|
body:not(.dark) .btn-info {
|
||||||
padding: 0.5rem 1rem;
|
color: white !important;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:not(.dark) .btn:hover,
|
body:not(.dark) .btn:hover,
|
||||||
body:not(.dark) button:not(.toggle):hover {
|
body:not(.dark) button:not(.toggle):hover {
|
||||||
background-color: var(--light-primary-hover);
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
|
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 */
|
/* Light Mode Cards und Panels */
|
||||||
@@ -545,27 +602,39 @@ body:not(.dark) .card:hover {
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Buttons */
|
/* Light Mode Buttons mit verbesserter Lesbarkeit */
|
||||||
body:not(.dark) .btn-primary {
|
body:not(.dark) .btn-primary {
|
||||||
background-color: var(--light-primary);
|
background: linear-gradient(135deg, #6d28d9, #5b21b6);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
transition: all 0.2s ease;
|
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 {
|
body:not(.dark) .btn-primary:hover {
|
||||||
background-color: var(--light-primary-hover);
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
|
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 {
|
body:not(.dark) .btn-secondary {
|
||||||
background-color: #f3f4f6;
|
background: linear-gradient(135deg, #ffffff, #f9fafb);
|
||||||
color: var(--light-text);
|
color: #1f2937;
|
||||||
border: 1px solid #e5e7eb;
|
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 {
|
body:not(.dark) .btn-secondary:hover {
|
||||||
background-color: #e5e7eb;
|
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 {
|
body:not(.dark) .btn-outline {
|
||||||
@@ -752,3 +821,248 @@ body:not(.dark) .user-dropdown {
|
|||||||
body:not(.dark) .user-dropdown-item:hover {
|
body:not(.dark) .user-dropdown-item:hover {
|
||||||
background-color: #f3f4f6;
|
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
437
static/css/mindmap.css
Normal 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
915
static/css/social.css
Normal 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;
|
||||||
|
}
|
||||||
11
static/img/default-avatar.svg
Normal file
11
static/img/default-avatar.svg
Normal 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 |
@@ -1,25 +1,54 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
"""
|
||||||
|
Generate favicon.ico from SVG using cairosvg and PIL
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
from cairosvg import svg2png
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import cairosvg
|
|
||||||
|
|
||||||
# Pfad zum SVG-Favicon
|
# Verzeichnis dieses Skripts
|
||||||
svg_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.svg')
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
# Ausgabepfad für das PNG
|
|
||||||
png_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.png')
|
|
||||||
# Ausgabepfad für das ICO
|
|
||||||
ico_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'favicon.ico')
|
|
||||||
|
|
||||||
# SVG zu PNG konvertieren
|
def svg_to_ico(svg_path, ico_path, sizes=[16, 32, 48, 64, 128, 256]):
|
||||||
cairosvg.svg2png(url=svg_path, write_to=png_path, output_width=512, output_height=512)
|
"""Convert SVG to multi-size ICO file"""
|
||||||
|
img_io = io.BytesIO()
|
||||||
|
|
||||||
# PNG zu ICO konvertieren
|
# Höchste Auflösung für Zwischenspeicherung
|
||||||
img = Image.open(png_path)
|
max_size = max(sizes)
|
||||||
img.save(ico_path, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128)])
|
|
||||||
|
|
||||||
print(f"Favicon erfolgreich erstellt: {ico_path}")
|
# 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)
|
||||||
|
|
||||||
# Optional: PNG-Datei löschen, wenn nur ICO benötigt wird
|
# PNG in verschiedene Größen konvertieren
|
||||||
# os.remove(png_path)
|
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!")
|
||||||
|
|
||||||
|
# Ursprüngliches Favicon konvertieren
|
||||||
|
svg_to_ico(
|
||||||
|
os.path.join(CURRENT_DIR, 'favicon.svg'),
|
||||||
|
os.path.join(CURRENT_DIR, 'favicon.ico')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Neues Neuron-Favicon konvertieren
|
||||||
|
svg_to_ico(
|
||||||
|
os.path.join(CURRENT_DIR, 'neuron-favicon.svg'),
|
||||||
|
os.path.join(CURRENT_DIR, 'neuron-favicon.ico')
|
||||||
|
)
|
||||||
29
static/img/neuron-favicon.svg
Normal file
29
static/img/neuron-favicon.svg
Normal 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 |
59
static/img/neuron-logo.svg
Normal file
59
static/img/neuron-logo.svg
Normal 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 |
@@ -1,420 +1,214 @@
|
|||||||
/**
|
/**
|
||||||
* Mindmap-Initialisierer
|
* Mindmap Initialisierung und Event-Handling
|
||||||
* Lädt und initialisiert die Mindmap-Visualisierung
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Warte bis DOM geladen ist
|
// Warte auf die Cytoscape-Instanz
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('mindmap-loaded', function() {
|
||||||
// Prüfe, ob wir auf der Mindmap-Seite sind
|
const cy = window.cy;
|
||||||
const cyContainer = document.getElementById('cy');
|
if (!cy) return;
|
||||||
|
|
||||||
if (!cyContainer) {
|
// Event-Listener für Knoten-Klicks
|
||||||
console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Initialisiere Mindmap-Visualisierung...');
|
|
||||||
|
|
||||||
// Prüfe, ob Cytoscape.js verfügbar ist
|
|
||||||
if (typeof cytoscape === 'undefined') {
|
|
||||||
loadScript('/static/js/cytoscape.min.js', initMindmap);
|
|
||||||
} else {
|
|
||||||
initMindmap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt ein Script dynamisch
|
|
||||||
* @param {string} src - Quelldatei
|
|
||||||
* @param {Function} callback - Callback nach dem Laden
|
|
||||||
*/
|
|
||||||
function loadScript(src, callback) {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = src;
|
|
||||||
script.onload = callback;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialisiert die Mindmap-Visualisierung
|
|
||||||
*/
|
|
||||||
function initMindmap() {
|
|
||||||
const cyContainer = document.getElementById('cy');
|
|
||||||
const fitBtn = document.getElementById('fit-btn');
|
|
||||||
const resetBtn = document.getElementById('reset-btn');
|
|
||||||
const toggleLabelsBtn = document.getElementById('toggle-labels-btn');
|
|
||||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
|
||||||
const nodeDescription = document.getElementById('node-description');
|
|
||||||
const connectedNodes = document.getElementById('connected-nodes');
|
|
||||||
|
|
||||||
let labelsVisible = true;
|
|
||||||
let selectedNode = null;
|
|
||||||
|
|
||||||
// Erstelle Cytoscape-Instanz
|
|
||||||
const cy = cytoscape({
|
|
||||||
container: cyContainer,
|
|
||||||
style: getDefaultStyles(),
|
|
||||||
layout: {
|
|
||||||
name: 'cose',
|
|
||||||
animate: true,
|
|
||||||
animationDuration: 800,
|
|
||||||
nodeDimensionsIncludeLabels: true,
|
|
||||||
padding: 50,
|
|
||||||
spacingFactor: 1.2,
|
|
||||||
randomize: true,
|
|
||||||
componentSpacing: 100,
|
|
||||||
nodeRepulsion: 8000,
|
|
||||||
edgeElasticity: 100,
|
|
||||||
nestingFactor: 1.2,
|
|
||||||
gravity: 80
|
|
||||||
},
|
|
||||||
wheelSensitivity: 0.3,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Daten vom Server laden
|
|
||||||
loadMindmapData(cy);
|
|
||||||
|
|
||||||
// Event-Handler zuweisen
|
|
||||||
setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt die Mindmap-Daten vom Server
|
|
||||||
* @param {Object} cy - Cytoscape-Instanz
|
|
||||||
*/
|
|
||||||
function loadMindmapData(cy) {
|
|
||||||
fetch('/api/mindmap')
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP Fehler: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (!data.nodes || data.nodes.length === 0) {
|
|
||||||
console.log('Keine Daten gefunden, versuche Refresh-API...');
|
|
||||||
return fetch('/api/refresh-mindmap')
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP Fehler beim Refresh: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log('Mindmap-Daten geladen:', data);
|
|
||||||
|
|
||||||
// Cytoscape-Elemente vorbereiten
|
|
||||||
const elements = [];
|
|
||||||
|
|
||||||
// Prüfen, ob "Wissen"-Knoten existiert
|
|
||||||
let rootNode = data.nodes.find(node => node.name === "Wissen");
|
|
||||||
|
|
||||||
// Wenn nicht, Root-Knoten hinzufügen
|
|
||||||
if (!rootNode) {
|
|
||||||
rootNode = {
|
|
||||||
id: 'root',
|
|
||||||
name: 'Wissen',
|
|
||||||
description: 'Zentrale Wissensbasis',
|
|
||||||
color_code: '#4299E1'
|
|
||||||
};
|
|
||||||
data.nodes.unshift(rootNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten hinzufügen
|
|
||||||
data.nodes.forEach(node => {
|
|
||||||
elements.push({
|
|
||||||
group: 'nodes',
|
|
||||||
data: {
|
|
||||||
id: node.id.toString(),
|
|
||||||
name: node.name,
|
|
||||||
description: node.description || '',
|
|
||||||
color: node.color_code || '#8B5CF6',
|
|
||||||
isRoot: node.name === 'Wissen'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kanten hinzufügen, wenn vorhanden
|
|
||||||
if (data.edges && data.edges.length > 0) {
|
|
||||||
data.edges.forEach(edge => {
|
|
||||||
elements.push({
|
|
||||||
group: 'edges',
|
|
||||||
data: {
|
|
||||||
id: `${edge.source}-${edge.target}`,
|
|
||||||
source: edge.source.toString(),
|
|
||||||
target: edge.target.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Wenn keine Kanten definiert sind, verbinde alle Knoten mit dem Root-Knoten
|
|
||||||
const rootId = rootNode.id.toString();
|
|
||||||
|
|
||||||
data.nodes.forEach(node => {
|
|
||||||
if (node.id.toString() !== rootId) {
|
|
||||||
elements.push({
|
|
||||||
group: 'edges',
|
|
||||||
data: {
|
|
||||||
id: `${rootId}-${node.id}`,
|
|
||||||
source: rootId,
|
|
||||||
target: node.id.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Elemente zu Cytoscape hinzufügen
|
|
||||||
cy.elements().remove();
|
|
||||||
cy.add(elements);
|
|
||||||
|
|
||||||
// Layout anwenden
|
|
||||||
cy.layout({
|
|
||||||
name: 'cose',
|
|
||||||
animate: true,
|
|
||||||
animationDuration: 800,
|
|
||||||
nodeDimensionsIncludeLabels: true,
|
|
||||||
padding: 50,
|
|
||||||
spacingFactor: 1.5,
|
|
||||||
randomize: false,
|
|
||||||
fit: true
|
|
||||||
}).run();
|
|
||||||
|
|
||||||
// Nach dem Laden Event auslösen
|
|
||||||
document.dispatchEvent(new CustomEvent('mindmap-loaded'));
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
|
||||||
|
|
||||||
// Fallback mit Standard-Daten
|
|
||||||
const fallbackData = {
|
|
||||||
nodes: [
|
|
||||||
{ id: 1, name: 'Wissen', description: 'Zentrale Wissensbasis', color_code: '#4299E1' },
|
|
||||||
{ id: 2, name: 'Philosophie', description: 'Philosophisches Denken', color_code: '#9F7AEA' },
|
|
||||||
{ id: 3, name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', color_code: '#48BB78' },
|
|
||||||
{ id: 4, name: 'Technologie', description: 'Technologische Entwicklungen', color_code: '#ED8936' },
|
|
||||||
{ id: 5, name: 'Künste', description: 'Künstlerische Ausdrucksformen', color_code: '#ED64A6' }
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{ source: 1, target: 2 },
|
|
||||||
{ source: 1, target: 3 },
|
|
||||||
{ source: 1, target: 4 },
|
|
||||||
{ source: 1, target: 5 }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const fallbackElements = [];
|
|
||||||
|
|
||||||
// Knoten hinzufügen
|
|
||||||
fallbackData.nodes.forEach(node => {
|
|
||||||
fallbackElements.push({
|
|
||||||
group: 'nodes',
|
|
||||||
data: {
|
|
||||||
id: node.id.toString(),
|
|
||||||
name: node.name,
|
|
||||||
description: node.description || '',
|
|
||||||
color: node.color_code || '#8B5CF6',
|
|
||||||
isRoot: node.name === 'Wissen'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kanten hinzufügen
|
|
||||||
fallbackData.edges.forEach(edge => {
|
|
||||||
fallbackElements.push({
|
|
||||||
group: 'edges',
|
|
||||||
data: {
|
|
||||||
id: `${edge.source}-${edge.target}`,
|
|
||||||
source: edge.source.toString(),
|
|
||||||
target: edge.target.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Elemente zu Cytoscape hinzufügen
|
|
||||||
cy.elements().remove();
|
|
||||||
cy.add(fallbackElements);
|
|
||||||
|
|
||||||
// Layout anwenden
|
|
||||||
cy.layout({ name: 'cose', animate: true }).run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Richtet Event-Listener für die Mindmap ein
|
|
||||||
* @param {Object} cy - Cytoscape-Instanz
|
|
||||||
* @param {HTMLElement} fitBtn - Fit-Button
|
|
||||||
* @param {HTMLElement} resetBtn - Reset-Button
|
|
||||||
* @param {HTMLElement} toggleLabelsBtn - Toggle-Labels-Button
|
|
||||||
* @param {HTMLElement} nodeInfoPanel - Node-Info-Panel
|
|
||||||
* @param {HTMLElement} nodeDescription - Node-Description
|
|
||||||
* @param {HTMLElement} connectedNodes - Connected-Nodes-Container
|
|
||||||
*/
|
|
||||||
function setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes) {
|
|
||||||
let labelsVisible = true;
|
|
||||||
|
|
||||||
// Fit-Button
|
|
||||||
if (fitBtn) {
|
|
||||||
fitBtn.addEventListener('click', function() {
|
|
||||||
cy.fit();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset-Button
|
|
||||||
if (resetBtn) {
|
|
||||||
resetBtn.addEventListener('click', function() {
|
|
||||||
cy.layout({ name: 'cose', animate: true }).run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle-Labels-Button
|
|
||||||
if (toggleLabelsBtn) {
|
|
||||||
toggleLabelsBtn.addEventListener('click', function() {
|
|
||||||
labelsVisible = !labelsVisible;
|
|
||||||
cy.style()
|
|
||||||
.selector('node')
|
|
||||||
.style({
|
|
||||||
'text-opacity': labelsVisible ? 1 : 0
|
|
||||||
})
|
|
||||||
.update();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knoten-Klick
|
|
||||||
cy.on('tap', 'node', function(evt) {
|
cy.on('tap', 'node', function(evt) {
|
||||||
const node = evt.target;
|
const node = evt.target;
|
||||||
|
|
||||||
// Zuvor ausgewählten Knoten zurücksetzen
|
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||||
cy.nodes().removeClass('selected');
|
cy.nodes().forEach(n => {
|
||||||
|
n.removeStyle();
|
||||||
// Neuen Knoten auswählen
|
n.connectedEdges().removeStyle();
|
||||||
node.addClass('selected');
|
|
||||||
|
|
||||||
if (nodeInfoPanel && nodeDescription && connectedNodes) {
|
|
||||||
// Info-Panel aktualisieren
|
|
||||||
nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.';
|
|
||||||
|
|
||||||
// Verbundene Knoten anzeigen
|
|
||||||
connectedNodes.innerHTML = '';
|
|
||||||
|
|
||||||
// Verbundene Knoten sammeln
|
|
||||||
const connectedNodesList = node.neighborhood('node');
|
|
||||||
|
|
||||||
if (connectedNodesList.length > 0) {
|
|
||||||
connectedNodesList.forEach(connectedNode => {
|
|
||||||
// Nicht den ausgewählten Knoten selbst anzeigen
|
|
||||||
if (connectedNode.id() !== node.id()) {
|
|
||||||
const nodeLink = document.createElement('span');
|
|
||||||
nodeLink.className = 'node-link';
|
|
||||||
nodeLink.textContent = connectedNode.data('name');
|
|
||||||
nodeLink.style.backgroundColor = connectedNode.data('color');
|
|
||||||
|
|
||||||
// Klick-Ereignis, um zu diesem Knoten zu wechseln
|
|
||||||
nodeLink.addEventListener('click', function() {
|
|
||||||
connectedNode.select();
|
|
||||||
cy.animate({
|
|
||||||
center: { eles: connectedNode },
|
|
||||||
duration: 500,
|
|
||||||
easing: 'ease-in-out-cubic'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
connectedNodes.appendChild(nodeLink);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
connectedNodes.innerHTML = '<em>Keine verbundenen Knoten</em>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panel anzeigen
|
|
||||||
nodeInfoPanel.classList.add('visible');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hintergrund-Klick
|
// Speichere ausgewählten Knoten
|
||||||
cy.on('tap', function(evt) {
|
window.mindmapInstance.selectedNode = node;
|
||||||
if (evt.target === cy) {
|
|
||||||
// Klick auf den Hintergrund
|
|
||||||
cy.nodes().removeClass('selected');
|
|
||||||
|
|
||||||
// Info-Panel verstecken
|
// Aktiviere leuchtenden Effekt statt Umkreisung
|
||||||
if (nodeInfoPanel) {
|
node.style({
|
||||||
nodeInfoPanel.classList.remove('visible');
|
'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
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dark Mode-Änderungen
|
// Verbundene Kanten und Knoten hervorheben
|
||||||
document.addEventListener('darkModeToggled', function(event) {
|
const connectedEdges = node.connectedEdges();
|
||||||
const isDark = event.detail.isDark;
|
const connectedNodes = node.neighborhood('node');
|
||||||
cy.style(getDefaultStyles(isDark));
|
|
||||||
|
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liefert die Standard-Stile für die Mindmap
|
* Aktualisiert die Seitenleiste mit Knoteninformationen
|
||||||
* @param {boolean} darkMode - Ob der Dark Mode aktiv ist
|
* @param {Object} node - Der ausgewählte Knoten
|
||||||
* @returns {Array} Array von Cytoscape-Stilen
|
|
||||||
*/
|
*/
|
||||||
function getDefaultStyles(darkMode = document.documentElement.classList.contains('dark')) {
|
function updateSidebar(node) {
|
||||||
return [
|
const sidebar = document.getElementById('sidebar');
|
||||||
{
|
if (!sidebar) return;
|
||||||
selector: 'node',
|
|
||||||
style: {
|
const data = node.data();
|
||||||
'background-color': 'data(color)',
|
const connectedNodes = node.neighborhood('node');
|
||||||
'label': 'data(name)',
|
|
||||||
'width': 40,
|
let html = `
|
||||||
'height': 40,
|
<div class="node-details">
|
||||||
'font-size': 12,
|
<h3>${data.label || data.name}</h3>
|
||||||
'text-valign': 'bottom',
|
<p class="category">${data.category || 'Keine Kategorie'}</p>
|
||||||
'text-halign': 'center',
|
${data.description ? `<p class="description">${data.description}</p>` : ''}
|
||||||
'text-margin-y': 8,
|
<div class="connections">
|
||||||
'color': darkMode ? '#f1f5f9' : '#334155',
|
<h4>Verbindungen (${connectedNodes.length})</h4>
|
||||||
'text-background-color': darkMode ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)',
|
<ul>
|
||||||
'text-background-opacity': 0.8,
|
`;
|
||||||
'text-background-padding': '2px',
|
|
||||||
'text-background-shape': 'roundrectangle',
|
connectedNodes.forEach(connectedNode => {
|
||||||
'text-wrap': 'ellipsis',
|
const connectedData = connectedNode.data();
|
||||||
'text-max-width': '100px'
|
html += `
|
||||||
}
|
<li style="color: ${connectedData.color || '#60a5fa'}">
|
||||||
},
|
${connectedData.label || connectedData.name}
|
||||||
{
|
</li>
|
||||||
selector: 'node[?isRoot]',
|
`;
|
||||||
style: {
|
});
|
||||||
'width': 60,
|
|
||||||
'height': 60,
|
html += `
|
||||||
'font-size': 14,
|
</ul>
|
||||||
'font-weight': 'bold',
|
</div>
|
||||||
'text-background-opacity': 0.9,
|
</div>
|
||||||
'text-background-color': '#4299E1'
|
`;
|
||||||
}
|
|
||||||
},
|
sidebar.innerHTML = html;
|
||||||
{
|
}
|
||||||
selector: 'edge',
|
|
||||||
style: {
|
/**
|
||||||
'width': 2,
|
* Setzt die Auswahl zurück
|
||||||
'line-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
* @param {Object} cy - Cytoscape-Instanz
|
||||||
'target-arrow-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)',
|
*/
|
||||||
'curve-style': 'bezier',
|
function resetSelection(cy) {
|
||||||
'target-arrow-shape': 'triangle'
|
window.mindmapInstance.selectedNode = null;
|
||||||
}
|
|
||||||
},
|
// Alle Hervorhebungen zurücksetzen
|
||||||
{
|
cy.nodes().forEach(node => {
|
||||||
selector: 'node.selected',
|
node.removeStyle();
|
||||||
style: {
|
node.connectedEdges().removeStyle();
|
||||||
'background-color': 'data(color)',
|
});
|
||||||
'border-width': 3,
|
|
||||||
'border-color': '#8b5cf6',
|
// Info-Panel ausblenden
|
||||||
'width': 50,
|
const infoPanel = document.getElementById('infoPanel');
|
||||||
'height': 50,
|
if (infoPanel) {
|
||||||
'font-size': 14,
|
infoPanel.style.display = 'none';
|
||||||
'font-weight': 'bold',
|
}
|
||||||
'text-background-color': '#8b5cf6',
|
|
||||||
'text-background-opacity': 0.9
|
// Seitenleiste leeren
|
||||||
}
|
const sidebar = document.getElementById('sidebar');
|
||||||
}
|
if (sidebar) {
|
||||||
];
|
sidebar.innerHTML = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
@@ -1,749 +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 = async endpoint => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint);
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
||||||
return []; // Leeres Array zurückgeben bei Fehlern
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fehler beim Abrufen von ${endpoint}:`, error);
|
|
||||||
return []; // Leeres Array zurückgeben bei Netzwerkfehlern
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const post = async (endpoint, body) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fehler beim POST zu ${endpoint}:`, error);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const del = async endpoint => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(endpoint, { method: 'DELETE' });
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`API-Fehler: ${endpoint} antwortet mit Status ${response.status}`);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Fehlern
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Fehler beim DELETE zu ${endpoint}:`, error);
|
|
||||||
return {}; // Leeres Objekt zurückgeben bei Netzwerkfehlern
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 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();
|
|
||||||
|
|
||||||
// Überprüfen, ob nodes ein Array ist, wenn nicht, setze es auf ein leeres Array
|
|
||||||
const nodesArray = Array.isArray(nodes) ? nodes : [];
|
|
||||||
|
|
||||||
// Knoten zum Graph hinzufügen
|
|
||||||
cy.add(
|
|
||||||
nodesArray.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
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Überprüfen, ob relationships ein Array ist, wenn nicht, setze es auf ein leeres Array
|
|
||||||
const relationshipsArray = Array.isArray(relationships) ? relationships : [];
|
|
||||||
|
|
||||||
// Kanten zum Graph hinzufügen
|
|
||||||
cy.add(
|
|
||||||
relationshipsArray.map(rel => ({
|
|
||||||
data: {
|
|
||||||
id: `${rel.parent_id}_${rel.child_id}`,
|
|
||||||
source: rel.parent_id.toString(),
|
|
||||||
target: rel.child_id.toString()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wenn keine Knoten geladen wurden, Fallback-Knoten erstellen
|
|
||||||
if (nodesArray.length === 0) {
|
|
||||||
// Mindestens einen Standardknoten hinzufügen
|
|
||||||
cy.add({
|
|
||||||
data: {
|
|
||||||
id: 'fallback-1',
|
|
||||||
name: 'Mindmap',
|
|
||||||
description: 'Erstellen Sie hier Ihre eigene Mindmap',
|
|
||||||
color: '#3b82f6',
|
|
||||||
icon: 'help-circle'
|
|
||||||
},
|
|
||||||
position: { x: 300, y: 200 }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Erfolgsmeldung anzeigen
|
|
||||||
console.log('Mindmap erfolgreich initialisiert mit Fallback-Knoten');
|
|
||||||
|
|
||||||
// Info-Meldung für Benutzer anzeigen
|
|
||||||
const infoBox = document.createElement('div');
|
|
||||||
infoBox.classList.add('info-message');
|
|
||||||
infoBox.style.position = 'absolute';
|
|
||||||
infoBox.style.top = '50%';
|
|
||||||
infoBox.style.left = '50%';
|
|
||||||
infoBox.style.transform = 'translate(-50%, -50%)';
|
|
||||||
infoBox.style.padding = '15px 20px';
|
|
||||||
infoBox.style.backgroundColor = 'rgba(59, 130, 246, 0.9)';
|
|
||||||
infoBox.style.color = 'white';
|
|
||||||
infoBox.style.borderRadius = '8px';
|
|
||||||
infoBox.style.zIndex = '5';
|
|
||||||
infoBox.style.maxWidth = '80%';
|
|
||||||
infoBox.style.textAlign = 'center';
|
|
||||||
infoBox.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
|
||||||
infoBox.innerHTML = 'Mindmap erfolgreich initialisiert.<br>Verwenden Sie die Werkzeugleiste, um Knoten hinzuzufügen.';
|
|
||||||
|
|
||||||
document.getElementById('cy').appendChild(infoBox);
|
|
||||||
|
|
||||||
// Meldung nach 5 Sekunden ausblenden
|
|
||||||
setTimeout(() => {
|
|
||||||
infoBox.style.opacity = '0';
|
|
||||||
infoBox.style.transition = 'opacity 0.5s ease';
|
|
||||||
setTimeout(() => {
|
|
||||||
if (infoBox.parentNode) {
|
|
||||||
infoBox.parentNode.removeChild(infoBox);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
})();
|
|
||||||
@@ -247,130 +247,63 @@ class ChatGPTAssistant {
|
|||||||
|
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
bubble.className = sender === 'user'
|
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%]'
|
? 'user-message 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%]';
|
: 'assistant-message rounded-lg py-2 px-3 max-w-[85%]';
|
||||||
|
|
||||||
// Formatierung des Texts (mit Markdown für Assistent-Nachrichten)
|
// Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen
|
||||||
let formattedText = '';
|
if (this.markdownParser) {
|
||||||
|
bubble.innerHTML = this.markdownParser.parse(text);
|
||||||
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('');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Für Benutzernachrichten einfache Formatierung
|
bubble.textContent = text;
|
||||||
formattedText = text.split('\n').map(line => {
|
|
||||||
if (line.trim() === '') return '<br>';
|
|
||||||
return `<p>${line}</p>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
messageEl.appendChild(bubble);
|
||||||
|
this.chatHistory.appendChild(messageEl);
|
||||||
|
|
||||||
if (this.chatHistory) {
|
// Scrolle zum Ende des Chat-Verlaufs
|
||||||
this.chatHistory.appendChild(messageEl);
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt Vorschläge als klickbare Pills an
|
* Zeigt Vorschläge für mögliche Fragen an
|
||||||
* @param {string[]} suggestions - Liste von Vorschlägen
|
* @param {Array} suggestions - Array von Vorschlägen
|
||||||
*/
|
*/
|
||||||
showSuggestions(suggestions) {
|
showSuggestions(suggestions) {
|
||||||
if (!this.suggestionArea) return;
|
if (!this.suggestionArea || !suggestions || !suggestions.length) return;
|
||||||
|
|
||||||
// Vorherige Vorschläge entfernen
|
// Vorherige Vorschläge entfernen
|
||||||
this.suggestionArea.innerHTML = '';
|
this.suggestionArea.innerHTML = '';
|
||||||
|
|
||||||
if (suggestions && suggestions.length > 0) {
|
// Neue Vorschläge hinzufügen
|
||||||
suggestions.forEach(suggestion => {
|
suggestions.forEach((text, index) => {
|
||||||
const pill = document.createElement('button');
|
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.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.textContent = suggestion;
|
pill.style.animationDelay = `${index * 0.1}s`;
|
||||||
this.suggestionArea.appendChild(pill);
|
pill.textContent = text;
|
||||||
});
|
this.suggestionArea.appendChild(pill);
|
||||||
|
});
|
||||||
|
|
||||||
this.suggestionArea.classList.remove('hidden');
|
// Vorschlagsbereich anzeigen
|
||||||
} else {
|
this.suggestionArea.classList.remove('hidden');
|
||||||
this.suggestionArea.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -409,14 +342,27 @@ class ChatGPTAssistant {
|
|||||||
messages: this.messages
|
messages: this.messages
|
||||||
}),
|
}),
|
||||||
cache: 'no-cache', // Kein Cache verwenden
|
cache: 'no-cache', // Kein Cache verwenden
|
||||||
credentials: 'same-origin' // Cookies senden
|
credentials: 'same-origin', // Cookies senden
|
||||||
|
timeout: 60000 // 60 Sekunden Timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ladeindikator entfernen
|
// Ladeindikator entfernen
|
||||||
this.removeLoadingIndicator();
|
this.removeLoadingIndicator();
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
const data = await response.json();
|
||||||
@@ -442,24 +388,45 @@ class ChatGPTAssistant {
|
|||||||
// Ladeindikator entfernen, falls noch vorhanden
|
// Ladeindikator entfernen, falls noch vorhanden
|
||||||
this.removeLoadingIndicator();
|
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
|
// Fehlermeldung anzeigen oder Wiederholungsversuch starten
|
||||||
if (this.retryCount < this.maxRetries) {
|
if (this.retryCount < this.maxRetries) {
|
||||||
this.retryCount++;
|
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
|
// Letzte Benutzernachricht speichern für den Wiederholungsversuch
|
||||||
setTimeout(() => {
|
const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user');
|
||||||
// Letzte Benutzernachricht aus dem Messages-Array entfernen
|
if (lastUserMessageIndex >= 0) {
|
||||||
const lastUserMessage = this.messages[this.messages.length - 2].content;
|
const lastUserMessage = this.messages[lastUserMessageIndex].content;
|
||||||
this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung
|
|
||||||
|
|
||||||
// Erneuter Versand mit gleicher Nachricht
|
// Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie
|
||||||
this.inputField.value = lastUserMessage;
|
const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ...
|
||||||
this.sendMessage();
|
|
||||||
}, 1500);
|
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 {
|
} else {
|
||||||
// Maximale Anzahl an Wiederholungsversuchen erreicht
|
// 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
|
this.retryCount = 0; // Zurücksetzen für die nächste Anfrage
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -512,26 +479,33 @@ class ChatGPTAssistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt einen Ladeindikator im Chat an
|
* Zeigt eine Ladeanimation an
|
||||||
*/
|
*/
|
||||||
showLoadingIndicator() {
|
showLoadingIndicator() {
|
||||||
if (!this.chatHistory) return;
|
if (!this.chatHistory) return;
|
||||||
|
|
||||||
// Entferne vorhandenen Ladeindikator (falls vorhanden)
|
// Prüfen, ob bereits ein Ladeindikator angezeigt wird
|
||||||
this.removeLoadingIndicator();
|
if (document.getElementById('assistant-loading-indicator')) return;
|
||||||
|
|
||||||
const loadingEl = document.createElement('div');
|
const loadingEl = document.createElement('div');
|
||||||
loadingEl.id = 'assistant-loading';
|
|
||||||
loadingEl.className = 'flex justify-start';
|
loadingEl.className = 'flex justify-start';
|
||||||
|
loadingEl.id = 'assistant-loading-indicator';
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
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.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center';
|
||||||
bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
|
||||||
|
|
||||||
|
const typingIndicator = document.createElement('div');
|
||||||
|
typingIndicator.className = 'typing-indicator';
|
||||||
|
typingIndicator.innerHTML = `
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
bubble.appendChild(typingIndicator);
|
||||||
loadingEl.appendChild(bubble);
|
loadingEl.appendChild(bubble);
|
||||||
this.chatHistory.appendChild(loadingEl);
|
|
||||||
|
|
||||||
// Scroll zum Ende des Verlaufs
|
this.chatHistory.appendChild(loadingEl);
|
||||||
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
this.chatHistory.scrollTop = this.chatHistory.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +513,7 @@ class ChatGPTAssistant {
|
|||||||
* Entfernt den Ladeindikator aus dem Chat
|
* Entfernt den Ladeindikator aus dem Chat
|
||||||
*/
|
*/
|
||||||
removeLoadingIndicator() {
|
removeLoadingIndicator() {
|
||||||
const loadingIndicator = document.getElementById('assistant-loading');
|
const loadingIndicator = document.getElementById('assistant-loading-indicator');
|
||||||
if (loadingIndicator) {
|
if (loadingIndicator) {
|
||||||
loadingIndicator.remove();
|
loadingIndicator.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,105 +26,216 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
function initMindmapPage() {
|
function initMindmapPage() {
|
||||||
console.log('Mindmap-Seite wird initialisiert...');
|
console.log('Mindmap-Seite wird initialisiert...');
|
||||||
|
|
||||||
// Hauptcontainer für die Mindmap
|
// Warte auf die Cytoscape-Instanz
|
||||||
const cyContainer = document.getElementById('cy');
|
document.addEventListener('mindmap-loaded', function() {
|
||||||
if (!cyContainer) {
|
const cy = window.cy;
|
||||||
console.error('Mindmap-Container #cy nicht gefunden!');
|
if (!cy) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info-Panel für Knotendetails
|
// Event-Listener für Knoten-Klicks
|
||||||
const nodeInfoPanel = document.getElementById('node-info-panel');
|
cy.on('tap', 'node', function(evt) {
|
||||||
const nodeDescription = document.getElementById('node-description');
|
const node = evt.target;
|
||||||
const connectedNodes = document.getElementById('connected-nodes');
|
|
||||||
|
|
||||||
// Toolbar-Buttons
|
// Alle vorherigen Hervorhebungen zurücksetzen
|
||||||
const fitButton = document.getElementById('fit-btn');
|
cy.nodes().forEach(n => {
|
||||||
const resetButton = document.getElementById('reset-btn');
|
n.removeStyle();
|
||||||
const toggleLabelsButton = document.getElementById('toggle-labels-btn');
|
n.connectedEdges().removeStyle();
|
||||||
|
|
||||||
// Mindmap-Instanz
|
|
||||||
let mindmap = null;
|
|
||||||
|
|
||||||
// Cytoscape.js für die Visualisierung initialisieren
|
|
||||||
try {
|
|
||||||
// Cytoscape.js-Bibliothek überprüfen
|
|
||||||
if (typeof cytoscape === 'undefined') {
|
|
||||||
loadExternalScript('https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js')
|
|
||||||
.then(() => {
|
|
||||||
initCytoscape();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Laden von Cytoscape.js:', error);
|
|
||||||
showErrorMessage(cyContainer, 'Cytoscape.js konnte nicht geladen werden.');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
initCytoscape();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler bei der Initialisierung der Mindmap:', error);
|
|
||||||
showErrorMessage(cyContainer, 'Die Mindmap konnte nicht initialisiert werden: ' + error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lädt ein externes Script asynchron
|
|
||||||
*/
|
|
||||||
function loadExternalScript(url) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = url;
|
|
||||||
script.onload = resolve;
|
|
||||||
script.onerror = reject;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zeigt eine Fehlermeldung im Container an
|
|
||||||
*/
|
|
||||||
function showErrorMessage(container, message) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="p-8 text-center bg-red-50 dark:bg-red-900/20 rounded-lg">
|
|
||||||
<i class="fa-solid fa-triangle-exclamation text-4xl text-red-500 mb-4"></i>
|
|
||||||
<p class="text-lg text-red-600 dark:text-red-400">${message}</p>
|
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Bitte laden Sie die Seite neu oder kontaktieren Sie den Support.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialisiert Cytoscape mit den Mindmap-Daten
|
|
||||||
*/
|
|
||||||
function initCytoscape() {
|
|
||||||
console.log('Cytoscape.js wird initialisiert...');
|
|
||||||
|
|
||||||
// Zeige Ladeanimation
|
|
||||||
cyContainer.innerHTML = `
|
|
||||||
<div class="flex justify-center items-center h-full">
|
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-purple-500"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Lade Daten vom Backend
|
|
||||||
fetch('/api/mindmap')
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Netzwerkfehler beim Laden der Mindmap-Daten');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log('Mindmap-Daten erfolgreich geladen');
|
|
||||||
renderMindmap(data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fehler beim Laden der Mindmap-Daten:', error);
|
|
||||||
|
|
||||||
// Verwende Standarddaten als Fallback
|
|
||||||
console.log('Verwende Standarddaten als Fallback...');
|
|
||||||
const defaultData = generateDefaultData();
|
|
||||||
renderMindmap(defaultData);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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 = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -435,4 +546,6 @@ function initMindmapPage() {
|
|||||||
];
|
];
|
||||||
return colors[Math.floor(Math.random() * colors.length)];
|
return colors[Math.floor(Math.random() * colors.length)];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Initialisiere die Mindmap-Seite
|
||||||
|
initMindmapPage();
|
||||||
Binary file not shown.
1133
static/js/social.js
Normal file
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
2724
static/js/update_mindmap.js
Normal file
File diff suppressed because it is too large
Load Diff
1078
static/js/update_mindmap.js.bak
Normal file
1078
static/js/update_mindmap.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
BIN
static/js/update_mindmap.js.new
Normal file
BIN
static/js/update_mindmap.js.new
Normal file
Binary file not shown.
1078
static/js/update_mindmap.js.original
Normal file
1078
static/js/update_mindmap.js.original
Normal file
File diff suppressed because it is too large
Load Diff
1904
static/mindmap.js
1904
static/mindmap.js
File diff suppressed because it is too large
Load Diff
@@ -18,11 +18,11 @@ class NeuralNetworkBackground {
|
|||||||
|
|
||||||
// Standardkonfiguration mit subtileren Werten
|
// Standardkonfiguration mit subtileren Werten
|
||||||
this.config = {
|
this.config = {
|
||||||
nodeCount: 30, // Weniger Knoten
|
nodeCount: 10, // Weniger Knoten
|
||||||
nodeSize: 1.2, // Kleinere Knoten
|
nodeSize: 1.2, // Kleinere Knoten
|
||||||
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
connectionDistance: 150, // Reduzierte Verbindungsdistanz
|
||||||
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
connectionOpacity: 0.3, // Sanftere Verbindungslinien
|
||||||
clusterCount: 6, // Weniger Cluster
|
clusterCount: 7, // Weniger Cluster
|
||||||
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
clusterRadius: 380, // Größerer Cluster-Radius für mehr Verteilung
|
||||||
animationSpeed: 0.25, // Langsamere Animation
|
animationSpeed: 0.25, // Langsamere Animation
|
||||||
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
flowDensity: 0.05, // Deutlich weniger Flussanimationen
|
||||||
@@ -1107,3 +1107,65 @@ window.addEventListener('beforeunload', () => {
|
|||||||
window.neuralNetworkBackground.destroy();
|
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();
|
||||||
|
}
|
||||||
@@ -41,15 +41,29 @@ class NeuralNetworkBackground {
|
|||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
this.isDestroying = false;
|
this.isDestroying = false;
|
||||||
|
|
||||||
// Farben - Lila Farbpalette
|
// Farben für Dark/Light Mode
|
||||||
this.colors = {
|
this.colors = {
|
||||||
background: '#040215',
|
dark: {
|
||||||
nodeColor: '#6a5498',
|
background: '#040215',
|
||||||
nodePulse: '#9c7fe0',
|
nodeColor: '#6a5498',
|
||||||
connectionColor: '#4a3870',
|
nodePulse: '#9c7fe0',
|
||||||
flowColor: '#b47fea'
|
connectionColor: '#4a3870',
|
||||||
|
flowColor: '#b47fea'
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
background: '#f8f9fc',
|
||||||
|
nodeColor: '#8c6db5',
|
||||||
|
nodePulse: '#b094dd',
|
||||||
|
connectionColor: '#9882bd',
|
||||||
|
flowColor: '#7d5bb5'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aktuelle Farbpalette basierend auf Theme
|
||||||
|
this.currentColors = document.documentElement.classList.contains('dark')
|
||||||
|
? this.colors.dark
|
||||||
|
: this.colors.light;
|
||||||
|
|
||||||
// Konfiguration
|
// Konfiguration
|
||||||
this.config = {
|
this.config = {
|
||||||
nodeCount: 80, // Anzahl der Knoten
|
nodeCount: 80, // Anzahl der Knoten
|
||||||
@@ -266,7 +280,11 @@ class NeuralNetworkBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(now) {
|
render(now) {
|
||||||
const colors = this.colors;
|
// Aktualisiere Farben basierend auf aktuellem Theme
|
||||||
|
this.currentColors = document.documentElement.classList.contains('dark')
|
||||||
|
? this.colors.dark
|
||||||
|
: this.colors.light;
|
||||||
|
const colors = this.currentColors;
|
||||||
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
||||||
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
@@ -386,7 +404,12 @@ class NeuralNetworkBackground {
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
window.neuralBackground = new NeuralNetworkBackground();
|
window.neuralBackground = new NeuralNetworkBackground();
|
||||||
|
|
||||||
// Sicherstellen, dass die Seite immer im Dark Mode ist
|
// Theme-Wechsel-Event-Listener
|
||||||
document.documentElement.classList.add('dark');
|
document.addEventListener('theme-changed', () => {
|
||||||
document.body.classList.add('dark');
|
if (window.neuralBackground) {
|
||||||
|
window.neuralBackground.currentColors = document.documentElement.classList.contains('dark')
|
||||||
|
? window.neuralBackground.colors.dark
|
||||||
|
: window.neuralBackground.colors.light;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
46
templates/admin/update_database.html
Normal file
46
templates/admin/update_database.html
Normal 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 %}
|
||||||
@@ -6,8 +6,7 @@
|
|||||||
<title>Systades - {% block title %}{% endblock %}</title>
|
<title>Systades - {% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- 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/neuron-favicon.svg') }}" type="image/svg+xml">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" sizes="any">
|
|
||||||
|
|
||||||
<!-- Meta Tags -->
|
<!-- Meta Tags -->
|
||||||
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
<meta name="description" content="Eine interaktive Plattform zum Visualisieren, Erforschen und Teilen von Wissen">
|
||||||
@@ -101,6 +100,9 @@
|
|||||||
<!-- Neural Network Background CSS -->
|
<!-- Neural Network Background CSS -->
|
||||||
<link href="{{ url_for('static', filename='css/neural-network-background.css') }}" rel="stylesheet">
|
<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 -->
|
<!-- D3.js für Visualisierungen -->
|
||||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
|
|
||||||
@@ -110,12 +112,6 @@
|
|||||||
<!-- ChatGPT Assistant -->
|
<!-- ChatGPT Assistant -->
|
||||||
<script src="{{ url_for('static', filename='js/modules/chatgpt-assistant.js') }}"></script>
|
<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 -->
|
<!-- Neural Network Background Script -->
|
||||||
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
<script src="{{ url_for('static', filename='neural-network-background.js') }}"></script>
|
||||||
|
|
||||||
@@ -130,13 +126,15 @@
|
|||||||
<style>
|
<style>
|
||||||
/* Light‑Mode */
|
/* Light‑Mode */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary:#f4f6fa;
|
--bg-primary:#f8fafc;
|
||||||
--bg-secondary:#e9ecf3;
|
--bg-secondary:#f1f5f9;
|
||||||
--text-primary:#232837;
|
--text-primary:#232837;
|
||||||
--text-secondary:#475569;
|
--text-secondary:#475569;
|
||||||
--accent-primary:#7c3aed;
|
--accent-primary:#7c3aed;
|
||||||
--accent-secondary:#8b5cf6;
|
--accent-secondary:#8b5cf6;
|
||||||
--glow-effect:0 0 8px rgba(139,92,246,.08);
|
--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;
|
||||||
}
|
}
|
||||||
/* Dark‑Mode */
|
/* Dark‑Mode */
|
||||||
.dark {
|
.dark {
|
||||||
@@ -150,7 +148,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
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 */
|
/* Utilities */
|
||||||
@@ -196,7 +195,119 @@
|
|||||||
body:not(.dark) .card:hover {
|
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);
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
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>
|
</head>
|
||||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||||
darkMode: true,
|
darkMode: true,
|
||||||
@@ -243,6 +354,7 @@
|
|||||||
this.darkMode = !this.darkMode;
|
this.darkMode = !this.darkMode;
|
||||||
this.applyDarkMode();
|
this.applyDarkMode();
|
||||||
|
|
||||||
|
// Server über Änderung informieren
|
||||||
fetch('/api/set_dark_mode', {
|
fetch('/api/set_dark_mode', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -253,11 +365,10 @@
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
// Event auslösen für andere Komponenten
|
||||||
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
document.dispatchEvent(new CustomEvent('darkModeToggled', {
|
||||||
detail: { isDark: this.darkMode }
|
detail: { isDark: this.darkMode }
|
||||||
}));
|
}));
|
||||||
} else {
|
|
||||||
console.error('Fehler beim Speichern der Dark Mode-Einstellung:', data.error);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -273,6 +384,7 @@
|
|||||||
<div class="container mx-auto flex justify-between items-center">
|
<div class="container mx-auto flex justify-between items-center">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<a href="{{ url_for('index') }}" class="flex items-center group">
|
<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>
|
<span class="text-2xl font-bold gradient-text transform transition-transform group-hover:scale-105">Systades</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -292,6 +404,22 @@
|
|||||||
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
||||||
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
||||||
</a>
|
</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') }}"
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
@@ -304,7 +432,7 @@
|
|||||||
class="nav-link flex items-center"
|
class="nav-link flex items-center"
|
||||||
x-bind:class="darkMode
|
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-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
|
<i class="fa-solid fa-robot mr-2"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -320,6 +448,14 @@
|
|||||||
|
|
||||||
<!-- Rechte Seite -->
|
<!-- Rechte Seite -->
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
|
<!-- 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 -->
|
<!-- Profil-Link oder Login -->
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<div class="relative" x-data="{ open: false }">
|
<div class="relative" x-data="{ open: false }">
|
||||||
@@ -333,12 +469,21 @@
|
|||||||
{% if current_user.avatar %}
|
{% if current_user.avatar %}
|
||||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm hidden lg:block">{{ current_user.username }}</span>
|
<span class="hidden md:block">{{ current_user.username }}</span>
|
||||||
<i class="fa-solid fa-chevron-down text-xs hidden lg:block transition-transform duration-200"
|
<i class="fas fa-chevron-down text-xs opacity-60 ml-1.5"></i>
|
||||||
x-bind:class="open ? 'transform rotate-180' : ''"></i>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Dropdown-Menü -->
|
<!-- Dropdown-Menü -->
|
||||||
@@ -444,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' }}'">
|
: '{{ '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
|
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
||||||
</a>
|
</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') }}"
|
<a href="{{ url_for('search_thoughts_page') }}"
|
||||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
x-bind:class="darkMode
|
||||||
@@ -456,7 +617,7 @@
|
|||||||
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
class="block w-full text-left py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||||
x-bind:class="darkMode
|
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-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
|
<i class="fa-solid fa-robot w-5 mr-3"></i>KI-Chat
|
||||||
</button>
|
</button>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
@@ -522,6 +683,10 @@
|
|||||||
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
Mindmap
|
Mindmap
|
||||||
</a>
|
</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 %}
|
{% if current_user.is_authenticated %}
|
||||||
<a href="{{ url_for('profile') }}" class="text-sm transition-all duration-200"
|
<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'">
|
:class="darkMode ? 'text-gray-300 hover:text-white' : 'text-gray-600 hover:text-gray-900'">
|
||||||
@@ -601,25 +766,207 @@
|
|||||||
|
|
||||||
<!-- Hilfsscripts -->
|
<!-- Hilfsscripts -->
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
|
|
||||||
<!-- KI-Chat Initialisierung -->
|
<!-- ChatGPT Initialisierung -->
|
||||||
<script>
|
<script>
|
||||||
// Initialisiere den ChatGPT-Assistenten direkt, um sicherzustellen,
|
// Prüfe, ob ChatGPTAssistant bereits existiert
|
||||||
// dass er auf jeder Seite verfügbar ist, selbst wenn MindMap nicht geladen ist
|
if (typeof ChatGPTAssistant === 'undefined') {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
class ChatGPTAssistant {
|
||||||
// Prüfen, ob der Assistent bereits durch MindMap initialisiert wurde
|
constructor() {
|
||||||
if (!window.MindMap || !window.MindMap.assistant) {
|
this.chatContainer = null;
|
||||||
console.log('KI-Assistent wird direkt initialisiert...');
|
this.messages = [];
|
||||||
const assistant = new ChatGPTAssistant();
|
this.isOpen = false;
|
||||||
assistant.init();
|
}
|
||||||
|
|
||||||
// Speichere in window.MindMap, falls es existiert, oder erstelle es
|
init() {
|
||||||
if (!window.MindMap) {
|
// Chat-Container erstellen, falls noch nicht vorhanden
|
||||||
window.MindMap = {};
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- Dark/Light-Mode vereinheitlicht -->
|
<!-- Dark/Light-Mode vereinheitlicht -->
|
||||||
@@ -627,36 +974,77 @@
|
|||||||
// Globaler Zugriff für externe Skripte
|
// Globaler Zugriff für externe Skripte
|
||||||
window.MindMap = window.MindMap || {};
|
window.MindMap = window.MindMap || {};
|
||||||
|
|
||||||
window.MindMap.toggleDarkMode = function() {
|
// Funktion zum Anwenden des Dark Mode, strikt getrennt
|
||||||
// Alpine.js-Instanz benutzen, wenn verfügbar
|
function applyDarkModeClasses(isDarkMode) {
|
||||||
|
if (isDarkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
localStorage.setItem('colorMode', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.body.classList.remove('dark');
|
||||||
|
localStorage.setItem('colorMode', 'light');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpine.js darkMode-Variable aktualisieren, falls zutreffend
|
||||||
const appEl = document.querySelector('body');
|
const appEl = document.querySelector('body');
|
||||||
if (appEl && appEl.__x) {
|
if (appEl && appEl.__x) {
|
||||||
appEl.__x.$data.toggleDarkMode();
|
appEl.__x.$data.darkMode = isDarkMode;
|
||||||
} else {
|
|
||||||
// Fallback: Nur classList und localStorage
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
|
||||||
document.documentElement.classList.toggle('dark', !isDark);
|
|
||||||
document.body.classList.toggle('dark', !isDark);
|
|
||||||
localStorage.setItem('colorMode', !isDark ? 'dark' : 'light');
|
|
||||||
|
|
||||||
// Server aktualisieren
|
|
||||||
fetch('/api/set_dark_mode', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ darkMode: !isDark })
|
|
||||||
}).catch(console.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback für Browser-Präferenz, falls keine Einstellung geladen werden konnte
|
// Initialisierung beim Laden
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (!document.body.classList.contains('dark') && !document.documentElement.classList.contains('dark')) {
|
// 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;
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
if (prefersDark) {
|
applyDarkModeClasses(prefersDark);
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
document.body.classList.add('dark');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -163,7 +163,17 @@
|
|||||||
{% if post.author.avatar %}
|
{% if post.author.avatar %}
|
||||||
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
|
<img src="{{ post.author.avatar }}" alt="{{ post.author.username }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ post.author.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(#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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -259,7 +269,17 @@
|
|||||||
{% if reply.author.avatar %}
|
{% if reply.author.avatar %}
|
||||||
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
|
<img src="{{ reply.author.avatar }}" alt="{{ reply.author.username }}" class="w-full h-full object-cover">
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ reply.author.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(#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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
365
templates/create_mindmap.html
Normal file
365
templates/create_mindmap.html
Normal 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
525
templates/edit_mindmap.html
Normal 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
48
templates/errors/400.html
Normal 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 %}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,23 @@
|
|||||||
</div>
|
</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 -->
|
<!-- Gemerkte Inhalte -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||||
<!-- Wissensbereiche -->
|
<!-- Wissensbereiche -->
|
||||||
@@ -123,6 +140,431 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- JavaScript für persönliche Mindmap -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
75
templates/simple_profile.html
Normal file
75
templates/simple_profile.html
Normal 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 %}
|
||||||
512
templates/social/discover.html
Normal file
512
templates/social/discover.html
Normal 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
327
templates/social/feed.html
Normal 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 %}
|
||||||
381
templates/social/notifications.html
Normal file
381
templates/social/notifications.html
Normal 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 %}
|
||||||
668
templates/social/profile.html
Normal file
668
templates/social/profile.html
Normal 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 %}
|
||||||
1579
templates/user_mindmap.html
Normal file
1579
templates/user_mindmap.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,8 @@ __all__ = [
|
|||||||
'delete_user',
|
'delete_user',
|
||||||
'create_admin_user',
|
'create_admin_user',
|
||||||
|
|
||||||
# Server management
|
# Server management (imported separately to avoid circular imports)
|
||||||
'run_development_server',
|
# 'run_development_server' - available in utils.server module
|
||||||
]
|
]
|
||||||
|
|
||||||
# Import remaining modules that might depend on app
|
# 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_rebuild import rebuild_database
|
||||||
from .db_test import test_database_connection, test_models, print_database_stats, run_all_tests
|
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 .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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/logger.cpython-311.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
38
utils/check_db.py
Normal file
38
utils/check_db.py
Normal 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()
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
import time
|
import time
|
||||||
|
|
||||||
def check_db_connection(db):
|
def check_db_connection(db, app=None):
|
||||||
"""
|
"""
|
||||||
Überprüft die Datenbankverbindung und versucht ggf. die Verbindung wiederherzustellen
|
Überprüft die Datenbankverbindung und versucht ggf. die Verbindung wiederherzustellen
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: SQLAlchemy-Instanz
|
db: SQLAlchemy-Instanz
|
||||||
|
app: Flask-App-Instanz (optional, falls nicht im App-Kontext)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True, wenn die Verbindung erfolgreich ist, sonst False
|
bool: True, wenn die Verbindung erfolgreich ist, sonst False
|
||||||
@@ -22,7 +22,11 @@ def check_db_connection(db):
|
|||||||
while retry_count < max_retries:
|
while retry_count < max_retries:
|
||||||
try:
|
try:
|
||||||
# Führe eine einfache Abfrage durch, um die Verbindung zu testen
|
# 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'))
|
db.session.execute(text('SELECT 1'))
|
||||||
return True
|
return True
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
@@ -38,42 +42,60 @@ def check_db_connection(db):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
except:
|
except:
|
||||||
pass
|
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
|
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
|
Initialisiert die Datenbank, falls erforderlich
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: SQLAlchemy-Instanz
|
db: SQLAlchemy-Instanz
|
||||||
initialize_function: Funktion, die aufgerufen wird, um die Datenbank zu initialisieren
|
initialize_function: Funktion, die aufgerufen wird, um die Datenbank zu initialisieren
|
||||||
|
app: Flask-App-Instanz (optional, falls nicht im App-Kontext)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True, wenn die Datenbank bereit ist, sonst False
|
bool: True, wenn die Datenbank bereit ist, sonst False
|
||||||
"""
|
"""
|
||||||
# Prüfe die Verbindung
|
# Prüfe die Verbindung
|
||||||
if not check_db_connection(db):
|
if not check_db_connection(db, app):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Prüfe, ob die Tabellen existieren
|
# Prüfe, ob die Tabellen existieren
|
||||||
try:
|
try:
|
||||||
with current_app.app_context():
|
if app:
|
||||||
# Führe eine Testabfrage auf einer Tabelle durch
|
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'))
|
db.session.execute(text('SELECT COUNT(*) FROM user'))
|
||||||
except SQLAlchemyError:
|
except SQLAlchemyError:
|
||||||
# Tabellen existieren nicht, erstelle sie
|
# Tabellen existieren nicht, erstelle sie
|
||||||
try:
|
try:
|
||||||
with current_app.app_context():
|
if app:
|
||||||
db.create_all()
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
# Rufe die Initialisierungsfunktion auf, falls vorhanden
|
# Rufe die Initialisierungsfunktion auf, falls vorhanden
|
||||||
|
if initialize_function and callable(initialize_function):
|
||||||
|
initialize_function()
|
||||||
|
else:
|
||||||
|
db.create_all()
|
||||||
if initialize_function and callable(initialize_function):
|
if initialize_function and callable(initialize_function):
|
||||||
initialize_function()
|
initialize_function()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler bei DB-Initialisierung: {str(e)}")
|
print(f"Fehler bei DB-Initialisierung: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Prüfen der Datenbank-Tabellen: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -11,19 +11,33 @@ import importlib.util
|
|||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
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
|
from models import db
|
||||||
|
|
||||||
def ensure_db_dir():
|
def ensure_db_dir():
|
||||||
"""Make sure the database directory exists."""
|
"""Make sure the database directory exists."""
|
||||||
|
db_path = get_db_path()
|
||||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||||
|
|
||||||
def fix_database_schema():
|
def fix_database_schema():
|
||||||
"""Fix the database schema by adding missing columns."""
|
"""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():
|
with app.app_context():
|
||||||
# Ensure directory exists
|
# Ensure directory exists
|
||||||
ensure_db_dir()
|
ensure_db_dir()
|
||||||
|
|
||||||
|
# Get database path
|
||||||
|
db_path = get_db_path()
|
||||||
|
|
||||||
# Check if database exists, create tables if needed
|
# Check if database exists, create tables if needed
|
||||||
if not os.path.exists(db_path):
|
if not os.path.exists(db_path):
|
||||||
print("Database doesn't exist. Creating all tables from scratch...")
|
print("Database doesn't exist. Creating all tables from scratch...")
|
||||||
|
|||||||
103
utils/db_operations.py
Normal file
103
utils/db_operations.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Verbindung zur Datenbank herstellen
|
||||||
|
db_path = os.path.join(os.getcwd(), 'database', 'systades.db')
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Schema der mind_map_node Tabelle anzeigen
|
||||||
|
cursor.execute("PRAGMA table_info(mind_map_node)")
|
||||||
|
columns = cursor.fetchall()
|
||||||
|
print("Tabellenschema mind_map_node:")
|
||||||
|
for column in columns:
|
||||||
|
print(f"{column[1]} ({column[2]})")
|
||||||
|
|
||||||
|
# Existierende Knoten anzeigen
|
||||||
|
cursor.execute("SELECT id, name, description, color_code FROM mind_map_node LIMIT 5")
|
||||||
|
existing_nodes = cursor.fetchall()
|
||||||
|
print("\nBestehende Knoten:")
|
||||||
|
for node in existing_nodes:
|
||||||
|
print(f"ID: {node[0]}, Name: {node[1]}, Beschreibung: {node[2]}")
|
||||||
|
|
||||||
|
# Mögliche Kategorien abrufen (für die Verknüpfung)
|
||||||
|
cursor.execute("SELECT id, name FROM category")
|
||||||
|
categories = cursor.fetchall()
|
||||||
|
print("\nVerfügbare Kategorien:")
|
||||||
|
for category in categories:
|
||||||
|
print(f"ID: {category[0]}, Name: {category[1]}")
|
||||||
|
|
||||||
|
# Wissenschaftliche Themengebiete für neue Knoten
|
||||||
|
scientific_nodes = [
|
||||||
|
{
|
||||||
|
"name": "Quantenphysik",
|
||||||
|
"description": "Die Quantenphysik befasst sich mit dem Verhalten von Materie und Energie auf atomarer und subatomarer Ebene.",
|
||||||
|
"color_code": "#4B0082", # Indigo
|
||||||
|
"icon": "fa-atom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Neurowissenschaften",
|
||||||
|
"description": "Interdisziplinäre Wissenschaft, die sich mit der Struktur und Funktion des Nervensystems und des Gehirns beschäftigt.",
|
||||||
|
"color_code": "#FF4500", # Orange-Rot
|
||||||
|
"icon": "fa-brain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Künstliche Intelligenz",
|
||||||
|
"description": "Forschungsgebiet der Informatik, das sich mit der Automatisierung intelligenten Verhaltens befasst.",
|
||||||
|
"color_code": "#008080", # Teal
|
||||||
|
"icon": "fa-robot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Klimaforschung",
|
||||||
|
"description": "Wissenschaftliche Untersuchung des Klimas, seiner Variationen und Veränderungen auf allen zeitlichen und räumlichen Skalen.",
|
||||||
|
"color_code": "#2E8B57", # Seegrün
|
||||||
|
"icon": "fa-cloud-sun"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Genetik",
|
||||||
|
"description": "Teilgebiet der Biologie, das sich mit Vererbung sowie der Funktion und Wirkung von Genen beschäftigt.",
|
||||||
|
"color_code": "#800080", # Lila
|
||||||
|
"icon": "fa-dna"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Astrophysik",
|
||||||
|
"description": "Zweig der Astronomie, der sich mit den physikalischen Eigenschaften des Universums befasst.",
|
||||||
|
"color_code": "#191970", # Mitternachtsblau
|
||||||
|
"icon": "fa-star"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Neue Knoten hinzufügen
|
||||||
|
print("\nFüge neue wissenschaftliche Knoten hinzu...")
|
||||||
|
for node in scientific_nodes:
|
||||||
|
# Prüfen, ob der Knoten bereits existiert
|
||||||
|
cursor.execute("SELECT id FROM mind_map_node WHERE name = ?", (node["name"],))
|
||||||
|
existing = cursor.fetchone()
|
||||||
|
if existing:
|
||||||
|
print(f"Knoten '{node['name']}' existiert bereits mit ID {existing[0]}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Zufällige Kategorie wählen, wenn vorhanden
|
||||||
|
category_id = None
|
||||||
|
if categories:
|
||||||
|
category_id = random.choice(categories)[0]
|
||||||
|
|
||||||
|
# Neuen Knoten einfügen
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO mind_map_node (name, description, color_code, icon, is_public, category_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""", (
|
||||||
|
node["name"],
|
||||||
|
node["description"],
|
||||||
|
node["color_code"],
|
||||||
|
node["icon"],
|
||||||
|
True,
|
||||||
|
category_id
|
||||||
|
))
|
||||||
|
print(f"Knoten '{node['name']}' hinzugefügt")
|
||||||
|
|
||||||
|
# Änderungen übernehmen und Verbindung schließen
|
||||||
|
conn.commit()
|
||||||
|
print("\nDatenbank erfolgreich aktualisiert!")
|
||||||
|
conn.close()
|
||||||
108
utils/db_test.py
108
utils/db_test.py
@@ -9,9 +9,19 @@ import sqlite3
|
|||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
from app import app, db_path
|
# Vermeidung zirkulärer Importe - importiere nur die Modelle und DB-Objekt
|
||||||
from models import db, User, Thought, MindMapNode, Category
|
from models import db, User, Thought, MindMapNode, Category
|
||||||
|
|
||||||
|
def get_db_path():
|
||||||
|
"""Ermittelt den Pfad zur Datenbankdatei"""
|
||||||
|
db_dir = os.path.join(parent_dir, 'database')
|
||||||
|
if not os.path.exists(db_dir):
|
||||||
|
os.makedirs(db_dir)
|
||||||
|
return os.path.join(db_dir, 'systades.db')
|
||||||
|
|
||||||
|
# Datenbank-Pfad
|
||||||
|
db_path = get_db_path()
|
||||||
|
|
||||||
def test_database_connection():
|
def test_database_connection():
|
||||||
"""Test if the database exists and can be connected to."""
|
"""Test if the database exists and can be connected to."""
|
||||||
try:
|
try:
|
||||||
@@ -37,52 +47,52 @@ def test_database_connection():
|
|||||||
|
|
||||||
def test_models():
|
def test_models():
|
||||||
"""Test if all models are properly defined and can be queried."""
|
"""Test if all models are properly defined and can be queried."""
|
||||||
with app.app_context():
|
# Import app here to avoid circular import
|
||||||
try:
|
from flask import current_app
|
||||||
print("\nTesting User model...")
|
try:
|
||||||
user_count = User.query.count()
|
print("\nTesting User model...")
|
||||||
print(f" Found {user_count} users")
|
user_count = User.query.count()
|
||||||
|
print(f" Found {user_count} users")
|
||||||
|
|
||||||
print("\nTesting Category model...")
|
print("\nTesting Category model...")
|
||||||
category_count = Category.query.count()
|
category_count = Category.query.count()
|
||||||
print(f" Found {category_count} categories")
|
print(f" Found {category_count} categories")
|
||||||
|
|
||||||
print("\nTesting MindMapNode model...")
|
print("\nTesting MindMapNode model...")
|
||||||
node_count = MindMapNode.query.count()
|
node_count = MindMapNode.query.count()
|
||||||
print(f" Found {node_count} mindmap nodes")
|
print(f" Found {node_count} mindmap nodes")
|
||||||
|
|
||||||
print("\nTesting Thought model...")
|
print("\nTesting Thought model...")
|
||||||
thought_count = Thought.query.count()
|
thought_count = Thought.query.count()
|
||||||
print(f" Found {thought_count} thoughts")
|
print(f" Found {thought_count} thoughts")
|
||||||
|
|
||||||
if user_count == 0:
|
if user_count == 0:
|
||||||
print("\nWARNING: No users found in the database. You might need to create an admin user.")
|
print("\nWARNING: No users found in the database. You might need to create an admin user.")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error testing models: {e}")
|
print(f"Error testing models: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def print_database_stats():
|
def print_database_stats():
|
||||||
"""Print database statistics."""
|
"""Print database statistics."""
|
||||||
with app.app_context():
|
try:
|
||||||
try:
|
stats = []
|
||||||
stats = []
|
stats.append(("Users", User.query.count()))
|
||||||
stats.append(("Users", User.query.count()))
|
stats.append(("Categories", Category.query.count()))
|
||||||
stats.append(("Categories", Category.query.count()))
|
stats.append(("Mindmap Nodes", MindMapNode.query.count()))
|
||||||
stats.append(("Mindmap Nodes", MindMapNode.query.count()))
|
stats.append(("Thoughts", Thought.query.count()))
|
||||||
stats.append(("Thoughts", Thought.query.count()))
|
|
||||||
|
|
||||||
print("\nDatabase Statistics:")
|
print("\nDatabase Statistics:")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
for name, count in stats:
|
for name, count in stats:
|
||||||
print(f"{name:<20} : {count}")
|
print(f"{name:<20} : {count}")
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error generating database statistics: {e}")
|
print(f"Error generating database statistics: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run_all_tests():
|
def run_all_tests():
|
||||||
"""Run all database tests."""
|
"""Run all database tests."""
|
||||||
@@ -97,15 +107,18 @@ def run_all_tests():
|
|||||||
if not test_database_connection():
|
if not test_database_connection():
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
# Test models
|
# Import app here to avoid circular import
|
||||||
print("\n2. Testing database models...")
|
from app import app
|
||||||
if not test_models():
|
with app.app_context():
|
||||||
success = False
|
# Test models
|
||||||
|
print("\n2. Testing database models...")
|
||||||
|
if not test_models():
|
||||||
|
success = False
|
||||||
|
|
||||||
# Print statistics
|
# Print statistics
|
||||||
print("\n3. Database statistics:")
|
print("\n3. Database statistics:")
|
||||||
if not print_database_stats():
|
if not print_database_stats():
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
if success:
|
if success:
|
||||||
@@ -117,4 +130,7 @@ def run_all_tests():
|
|||||||
return success
|
return success
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run_all_tests()
|
# Import app here to avoid circular import
|
||||||
|
from app import app
|
||||||
|
with app.app_context():
|
||||||
|
run_all_tests()
|
||||||
BIN
utils/fix_routes.py
Normal file
BIN
utils/fix_routes.py
Normal file
Binary file not shown.
792
utils/logger.py
Normal file
792
utils/logger.py
Normal file
@@ -0,0 +1,792 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import wraps
|
||||||
|
from flask import request, g, current_app
|
||||||
|
from flask_login import current_user
|
||||||
|
import traceback
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
# ANSI Color Codes für farbige Terminal-Ausgabe
|
||||||
|
class Colors:
|
||||||
|
# Standard Colors
|
||||||
|
RESET = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
DIM = '\033[2m'
|
||||||
|
|
||||||
|
# Foreground Colors
|
||||||
|
BLACK = '\033[30m'
|
||||||
|
RED = '\033[31m'
|
||||||
|
GREEN = '\033[32m'
|
||||||
|
YELLOW = '\033[33m'
|
||||||
|
BLUE = '\033[34m'
|
||||||
|
MAGENTA = '\033[35m'
|
||||||
|
CYAN = '\033[36m'
|
||||||
|
WHITE = '\033[37m'
|
||||||
|
|
||||||
|
# Bright Colors
|
||||||
|
BRIGHT_RED = '\033[91m'
|
||||||
|
BRIGHT_GREEN = '\033[92m'
|
||||||
|
BRIGHT_YELLOW = '\033[93m'
|
||||||
|
BRIGHT_BLUE = '\033[94m'
|
||||||
|
BRIGHT_MAGENTA = '\033[95m'
|
||||||
|
BRIGHT_CYAN = '\033[96m'
|
||||||
|
BRIGHT_WHITE = '\033[97m'
|
||||||
|
|
||||||
|
# Background Colors
|
||||||
|
BG_RED = '\033[41m'
|
||||||
|
BG_GREEN = '\033[42m'
|
||||||
|
BG_YELLOW = '\033[43m'
|
||||||
|
BG_BLUE = '\033[44m'
|
||||||
|
BG_MAGENTA = '\033[45m'
|
||||||
|
BG_CYAN = '\033[46m'
|
||||||
|
|
||||||
|
class LoggerConfig:
|
||||||
|
"""Konfiguration für das Logging-System"""
|
||||||
|
LOG_DIR = 'logs'
|
||||||
|
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB
|
||||||
|
BACKUP_COUNT = 5
|
||||||
|
LOG_FORMAT = '%(asctime)s | %(levelname)s | %(name)s | %(message)s'
|
||||||
|
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||||
|
|
||||||
|
class ColoredFormatter(logging.Formatter):
|
||||||
|
"""Custom Formatter für farbige Log-Ausgaben mit schönen Emojis"""
|
||||||
|
|
||||||
|
LEVEL_COLORS = {
|
||||||
|
'DEBUG': Colors.BRIGHT_CYAN,
|
||||||
|
'INFO': Colors.BRIGHT_GREEN,
|
||||||
|
'WARNING': Colors.BRIGHT_YELLOW,
|
||||||
|
'ERROR': Colors.BRIGHT_RED,
|
||||||
|
'CRITICAL': Colors.BG_RED + Colors.BRIGHT_WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
COMPONENT_COLORS = {
|
||||||
|
'AUTH': Colors.BLUE,
|
||||||
|
'API': Colors.GREEN,
|
||||||
|
'DB': Colors.MAGENTA,
|
||||||
|
'SOCIAL': Colors.CYAN,
|
||||||
|
'SYSTEM': Colors.YELLOW,
|
||||||
|
'ERROR': Colors.RED,
|
||||||
|
'SECURITY': Colors.BRIGHT_MAGENTA,
|
||||||
|
'PERFORMANCE': Colors.BRIGHT_BLUE,
|
||||||
|
'ACTIVITY': Colors.BRIGHT_CYAN
|
||||||
|
}
|
||||||
|
|
||||||
|
# Erweiterte Emoji-Mappings für verschiedene Komponenten und Aktionen
|
||||||
|
COMPONENT_EMOJIS = {
|
||||||
|
'AUTH': '🔐',
|
||||||
|
'API': '🌐',
|
||||||
|
'DB': '🗄️',
|
||||||
|
'SOCIAL': '👥',
|
||||||
|
'SYSTEM': '⚙️',
|
||||||
|
'ERROR': '💥',
|
||||||
|
'SECURITY': '🛡️',
|
||||||
|
'PERFORMANCE': '⚡',
|
||||||
|
'ACTIVITY': '🎯'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spezielle Emojis für verschiedene Log-Level
|
||||||
|
LEVEL_EMOJIS = {
|
||||||
|
'DEBUG': '🔍',
|
||||||
|
'INFO': '✅',
|
||||||
|
'WARNING': '⚠️',
|
||||||
|
'ERROR': '❌',
|
||||||
|
'CRITICAL': '🚨'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Action-spezifische Emojis
|
||||||
|
ACTION_EMOJIS = {
|
||||||
|
'login': '🚪',
|
||||||
|
'logout': '🚪',
|
||||||
|
'register': '📝',
|
||||||
|
'like': '❤️',
|
||||||
|
'unlike': '💔',
|
||||||
|
'comment': '💬',
|
||||||
|
'share': '🔄',
|
||||||
|
'follow': '➕',
|
||||||
|
'unfollow': '➖',
|
||||||
|
'bookmark': '🔖',
|
||||||
|
'unbookmark': '📑',
|
||||||
|
'post_created': '📝',
|
||||||
|
'post_deleted': '🗑️',
|
||||||
|
'upload': '📤',
|
||||||
|
'download': '📥',
|
||||||
|
'search': '🔍',
|
||||||
|
'notification': '🔔',
|
||||||
|
'message': '💌',
|
||||||
|
'profile_update': '👤',
|
||||||
|
'settings': '⚙️',
|
||||||
|
'admin': '👑',
|
||||||
|
'backup': '💾',
|
||||||
|
'restore': '🔄',
|
||||||
|
'migration': '🚚',
|
||||||
|
'cache': '⚡',
|
||||||
|
'email': '📧',
|
||||||
|
'password_reset': '🔑',
|
||||||
|
'verification': '✅',
|
||||||
|
'ban': '🚫',
|
||||||
|
'unban': '✅',
|
||||||
|
'report': '🚩',
|
||||||
|
'moderate': '🛡️'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_action_emoji(self, message: str) -> str:
|
||||||
|
"""Ermittelt das passende Emoji basierend auf der Nachricht"""
|
||||||
|
message_lower = message.lower()
|
||||||
|
for action, emoji in self.ACTION_EMOJIS.items():
|
||||||
|
if action in message_lower:
|
||||||
|
return emoji
|
||||||
|
return '📝'
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
# Zeitstempel mit schöner Formatierung
|
||||||
|
timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3]
|
||||||
|
colored_timestamp = f"{Colors.DIM}⏰ {timestamp}{Colors.RESET}"
|
||||||
|
|
||||||
|
# Level mit Farbe und Emoji
|
||||||
|
level_color = self.LEVEL_COLORS.get(record.levelname, Colors.WHITE)
|
||||||
|
level_emoji = self.LEVEL_EMOJIS.get(record.levelname, '📝')
|
||||||
|
colored_level = f"{level_color}{level_emoji} {record.levelname:<8}{Colors.RESET}"
|
||||||
|
|
||||||
|
# Component mit Farbe und Emoji
|
||||||
|
component = getattr(record, 'component', 'SYSTEM')
|
||||||
|
component_color = self.COMPONENT_COLORS.get(component, Colors.WHITE)
|
||||||
|
component_emoji = self.COMPONENT_EMOJIS.get(component, '📝')
|
||||||
|
colored_component = f"{component_color}{component_emoji} [{component:<11}]{Colors.RESET}"
|
||||||
|
|
||||||
|
# Message mit Action-spezifischem Emoji
|
||||||
|
message = record.getMessage()
|
||||||
|
action_emoji = self._get_action_emoji(message)
|
||||||
|
|
||||||
|
# User-Info hinzufügen falls verfügbar
|
||||||
|
user_info = ""
|
||||||
|
if hasattr(record, 'user') and record.user:
|
||||||
|
user_info = f" {Colors.BRIGHT_BLUE}👤 {record.user}{Colors.RESET}"
|
||||||
|
|
||||||
|
# IP-Info hinzufügen falls verfügbar
|
||||||
|
ip_info = ""
|
||||||
|
if hasattr(record, 'ip') and record.ip:
|
||||||
|
ip_info = f" {Colors.DIM}🌍 {record.ip}{Colors.RESET}"
|
||||||
|
|
||||||
|
# Duration-Info hinzufügen falls verfügbar
|
||||||
|
duration_info = ""
|
||||||
|
if hasattr(record, 'duration') and record.duration:
|
||||||
|
if record.duration > 1000:
|
||||||
|
duration_color = Colors.BRIGHT_RED
|
||||||
|
duration_emoji = "🐌"
|
||||||
|
elif record.duration > 500:
|
||||||
|
duration_color = Colors.BRIGHT_YELLOW
|
||||||
|
duration_emoji = "⏱️"
|
||||||
|
else:
|
||||||
|
duration_color = Colors.BRIGHT_GREEN
|
||||||
|
duration_emoji = "⚡"
|
||||||
|
duration_info = f" {duration_color}{duration_emoji} {record.duration:.2f}ms{Colors.RESET}"
|
||||||
|
|
||||||
|
# Separator für bessere Lesbarkeit
|
||||||
|
separator = f"{Colors.DIM}│{Colors.RESET}"
|
||||||
|
|
||||||
|
# Finale formatierte Nachricht mit schöner Struktur
|
||||||
|
formatted_message = (
|
||||||
|
f"{colored_timestamp} {separator} "
|
||||||
|
f"{colored_level} {separator} "
|
||||||
|
f"{colored_component} {separator} "
|
||||||
|
f"{action_emoji} {message}"
|
||||||
|
f"{user_info}{ip_info}{duration_info}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return formatted_message
|
||||||
|
|
||||||
|
class JSONFormatter(logging.Formatter):
|
||||||
|
"""JSON-Formatter für strukturierte Logs"""
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
log_entry = {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'level': record.levelname,
|
||||||
|
'module': record.module,
|
||||||
|
'function': record.funcName,
|
||||||
|
'line': record.lineno,
|
||||||
|
'message': record.getMessage(),
|
||||||
|
'logger': record.name
|
||||||
|
}
|
||||||
|
|
||||||
|
# Benutzerinformationen hinzufügen - nur im Application Context
|
||||||
|
try:
|
||||||
|
from flask import has_app_context, g, current_user
|
||||||
|
if has_app_context():
|
||||||
|
if hasattr(g, 'user_id'):
|
||||||
|
log_entry['user_id'] = g.user_id
|
||||||
|
elif current_user and hasattr(current_user, 'id') and current_user.is_authenticated:
|
||||||
|
log_entry['user_id'] = current_user.id
|
||||||
|
except (ImportError, RuntimeError):
|
||||||
|
# Flask ist nicht verfügbar oder kein App-Context
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Request-Informationen hinzufügen - nur im Request Context
|
||||||
|
try:
|
||||||
|
from flask import has_request_context, request
|
||||||
|
if has_request_context() and request:
|
||||||
|
log_entry['request'] = {
|
||||||
|
'method': getattr(request, 'method', None),
|
||||||
|
'path': getattr(request, 'path', None),
|
||||||
|
'remote_addr': getattr(request, 'remote_addr', None),
|
||||||
|
'user_agent': str(getattr(request, 'user_agent', ''))
|
||||||
|
}
|
||||||
|
except (ImportError, RuntimeError):
|
||||||
|
# Flask ist nicht verfügbar oder kein Request-Context
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Performance-Informationen hinzufügen - nur im Application Context
|
||||||
|
try:
|
||||||
|
from flask import has_app_context, g
|
||||||
|
if has_app_context() and hasattr(g, 'start_time'):
|
||||||
|
duration = (datetime.now() - g.start_time).total_seconds() * 1000
|
||||||
|
log_entry['duration_ms'] = round(duration, 2)
|
||||||
|
except (ImportError, RuntimeError):
|
||||||
|
# Flask ist nicht verfügbar oder kein App-Context
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Exception-Informationen hinzufügen
|
||||||
|
if record.exc_info:
|
||||||
|
log_entry['exception'] = {
|
||||||
|
'type': record.exc_info[0].__name__,
|
||||||
|
'message': str(record.exc_info[1]),
|
||||||
|
'traceback': self.formatException(record.exc_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.dumps(log_entry)
|
||||||
|
|
||||||
|
class SocialNetworkLogger:
|
||||||
|
"""Hauptklasse für das Social Network Logging"""
|
||||||
|
|
||||||
|
def __init__(self, name: str = 'SysTades'):
|
||||||
|
self.name = name
|
||||||
|
self.logger = logging.getLogger(name)
|
||||||
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Log-Verzeichnis erstellen
|
||||||
|
os.makedirs(LoggerConfig.LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Handler nur einmal hinzufügen
|
||||||
|
if not self.logger.handlers:
|
||||||
|
self._setup_handlers()
|
||||||
|
|
||||||
|
def _setup_handlers(self):
|
||||||
|
"""Setup für verschiedene Log-Handler"""
|
||||||
|
|
||||||
|
# Console Handler mit Farben
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.DEBUG)
|
||||||
|
console_handler.setFormatter(ColoredFormatter())
|
||||||
|
self.logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# File Handler für alle Logs
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
os.path.join(LoggerConfig.LOG_DIR, 'app.log'),
|
||||||
|
maxBytes=LoggerConfig.MAX_LOG_SIZE,
|
||||||
|
backupCount=LoggerConfig.BACKUP_COUNT,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
file_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s | %(levelname)s | %(name)s | %(component)s | %(message)s',
|
||||||
|
datefmt=LoggerConfig.DATE_FORMAT
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(file_formatter)
|
||||||
|
self.logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Error Handler für nur Fehler
|
||||||
|
error_handler = RotatingFileHandler(
|
||||||
|
os.path.join(LoggerConfig.LOG_DIR, 'errors.log'),
|
||||||
|
maxBytes=LoggerConfig.MAX_LOG_SIZE,
|
||||||
|
backupCount=LoggerConfig.BACKUP_COUNT,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
error_handler.setLevel(logging.ERROR)
|
||||||
|
error_handler.setFormatter(file_formatter)
|
||||||
|
self.logger.addHandler(error_handler)
|
||||||
|
|
||||||
|
# API Handler für API-spezifische Logs
|
||||||
|
api_handler = RotatingFileHandler(
|
||||||
|
os.path.join(LoggerConfig.LOG_DIR, 'api.log'),
|
||||||
|
maxBytes=LoggerConfig.MAX_LOG_SIZE,
|
||||||
|
backupCount=LoggerConfig.BACKUP_COUNT,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
api_handler.setLevel(logging.INFO)
|
||||||
|
api_handler.addFilter(lambda record: hasattr(record, 'component') and record.component == 'API')
|
||||||
|
api_handler.setFormatter(file_formatter)
|
||||||
|
self.logger.addHandler(api_handler)
|
||||||
|
|
||||||
|
def _log_with_context(self, level: str, message: str, component: str = 'SYSTEM', **kwargs):
|
||||||
|
"""Log mit erweiterten Kontext-Informationen"""
|
||||||
|
extra = {'component': component}
|
||||||
|
|
||||||
|
# User-Info hinzufügen
|
||||||
|
if 'user' in kwargs:
|
||||||
|
extra['user'] = kwargs['user']
|
||||||
|
|
||||||
|
# IP-Info hinzufügen
|
||||||
|
if 'ip' in kwargs:
|
||||||
|
extra['ip'] = kwargs['ip']
|
||||||
|
|
||||||
|
# Duration-Info hinzufügen
|
||||||
|
if 'duration' in kwargs:
|
||||||
|
extra['duration'] = kwargs['duration']
|
||||||
|
|
||||||
|
# Weitere Context-Daten
|
||||||
|
extra.update({k: v for k, v in kwargs.items() if k not in ['user', 'ip', 'duration']})
|
||||||
|
|
||||||
|
getattr(self.logger, level.lower())(message, extra=extra)
|
||||||
|
|
||||||
|
def debug(self, message: str, component: str = 'SYSTEM', **kwargs):
|
||||||
|
"""Debug-Level Logging mit erweiterten Infos"""
|
||||||
|
self._log_with_context('DEBUG', message, component, **kwargs)
|
||||||
|
|
||||||
|
def info(self, message: str, component: str = 'SYSTEM', **kwargs):
|
||||||
|
"""Info-Level Logging mit erweiterten Infos"""
|
||||||
|
self._log_with_context('INFO', message, component, **kwargs)
|
||||||
|
|
||||||
|
def warning(self, message: str, component: str = 'SYSTEM', **kwargs):
|
||||||
|
"""Warning-Level Logging mit erweiterten Infos"""
|
||||||
|
self._log_with_context('WARNING', message, component, **kwargs)
|
||||||
|
|
||||||
|
def error(self, message: str, component: str = 'ERROR', **kwargs):
|
||||||
|
"""Error-Level Logging mit erweiterten Infos"""
|
||||||
|
self._log_with_context('ERROR', message, component, **kwargs)
|
||||||
|
|
||||||
|
def critical(self, message: str, component: str = 'ERROR', **kwargs):
|
||||||
|
"""Critical-Level Logging mit erweiterten Infos"""
|
||||||
|
self._log_with_context('CRITICAL', message, component, **kwargs)
|
||||||
|
|
||||||
|
# Erweiterte spezielle Logging-Methoden für Social Network
|
||||||
|
|
||||||
|
def auth_success(self, username: str, ip: str = None, method: str = 'password'):
|
||||||
|
"""Erfolgreiche Authentifizierung mit Details"""
|
||||||
|
message = f"Benutzer '{username}' erfolgreich angemeldet"
|
||||||
|
if method != 'password':
|
||||||
|
message += f" (Methode: {method})"
|
||||||
|
self.info(message, 'AUTH', user=username, ip=ip)
|
||||||
|
|
||||||
|
def auth_failure(self, username: str, ip: str = None, reason: str = None, method: str = 'password'):
|
||||||
|
"""Fehlgeschlagene Authentifizierung mit Details"""
|
||||||
|
message = f"Anmeldung fehlgeschlagen für '{username}'"
|
||||||
|
if reason:
|
||||||
|
message += f" - Grund: {reason}"
|
||||||
|
if method != 'password':
|
||||||
|
message += f" (Methode: {method})"
|
||||||
|
self.warning(message, 'AUTH', user=username, ip=ip)
|
||||||
|
|
||||||
|
def user_action(self, username: str, action: str, details: str = None, target: str = None):
|
||||||
|
"""Erweiterte Benutzer-Aktion mit mehr Details"""
|
||||||
|
message = f"{username}: {action}"
|
||||||
|
if target:
|
||||||
|
message += f" → {target}"
|
||||||
|
if details:
|
||||||
|
message += f" ({details})"
|
||||||
|
self.info(message, 'ACTIVITY', user=username)
|
||||||
|
|
||||||
|
def api_request(self, method: str, endpoint: str, user: str = None, status: int = None, duration: float = None, size: int = None):
|
||||||
|
"""Erweiterte API Request Logging"""
|
||||||
|
message = f"{method} {endpoint}"
|
||||||
|
|
||||||
|
# Status-spezifische Emojis und Farben
|
||||||
|
if status:
|
||||||
|
if status >= 500:
|
||||||
|
message = f"Server Error: {message}"
|
||||||
|
component = 'ERROR'
|
||||||
|
elif status >= 400:
|
||||||
|
message = f"Client Error: {message}"
|
||||||
|
component = 'API'
|
||||||
|
elif status >= 300:
|
||||||
|
message = f"Redirect: {message}"
|
||||||
|
component = 'API'
|
||||||
|
else:
|
||||||
|
message = f"Success: {message}"
|
||||||
|
component = 'API'
|
||||||
|
else:
|
||||||
|
component = 'API'
|
||||||
|
|
||||||
|
# Zusätzliche Infos
|
||||||
|
extras = {}
|
||||||
|
if user:
|
||||||
|
extras['user'] = user
|
||||||
|
if duration:
|
||||||
|
extras['duration'] = duration * 1000 # Convert to ms
|
||||||
|
if size:
|
||||||
|
message += f" ({self._format_bytes(size)})"
|
||||||
|
|
||||||
|
if status and status >= 400:
|
||||||
|
self.warning(message, component, **extras)
|
||||||
|
else:
|
||||||
|
self.info(message, component, **extras)
|
||||||
|
|
||||||
|
def database_operation(self, operation: str, table: str, success: bool = True, details: str = None, affected_rows: int = None):
|
||||||
|
"""Erweiterte Datenbank-Operation Logging"""
|
||||||
|
message = f"DB {operation.upper()} auf '{table}'"
|
||||||
|
|
||||||
|
if affected_rows is not None:
|
||||||
|
message += f" ({affected_rows} Zeilen)"
|
||||||
|
|
||||||
|
if details:
|
||||||
|
message += f" - {details}"
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.info(message, 'DB')
|
||||||
|
else:
|
||||||
|
self.error(message, 'DB')
|
||||||
|
|
||||||
|
def security_event(self, event: str, user: str = None, ip: str = None, severity: str = 'warning', details: str = None):
|
||||||
|
"""Erweiterte Sicherheitsereignis Logging"""
|
||||||
|
message = f"Security Event: {event}"
|
||||||
|
if details:
|
||||||
|
message += f" - {details}"
|
||||||
|
|
||||||
|
extras = {}
|
||||||
|
if user:
|
||||||
|
extras['user'] = user
|
||||||
|
if ip:
|
||||||
|
extras['ip'] = ip
|
||||||
|
|
||||||
|
if severity == 'critical':
|
||||||
|
self.critical(message, 'SECURITY', **extras)
|
||||||
|
elif severity == 'error':
|
||||||
|
self.error(message, 'SECURITY', **extras)
|
||||||
|
else:
|
||||||
|
self.warning(message, 'SECURITY', **extras)
|
||||||
|
|
||||||
|
def performance_metric(self, metric_name: str, value: float, unit: str = 'ms', threshold: dict = None):
|
||||||
|
"""Erweiterte Performance-Metrik Logging"""
|
||||||
|
message = f"Performance: {metric_name} = {value}{unit}"
|
||||||
|
|
||||||
|
# Threshold-basierte Bewertung
|
||||||
|
if threshold and unit == 'ms':
|
||||||
|
if value > threshold.get('critical', 2000):
|
||||||
|
self.critical(message, 'PERFORMANCE', duration=value)
|
||||||
|
elif value > threshold.get('warning', 1000):
|
||||||
|
self.warning(message, 'PERFORMANCE', duration=value)
|
||||||
|
else:
|
||||||
|
self.info(message, 'PERFORMANCE', duration=value)
|
||||||
|
else:
|
||||||
|
self.info(message, 'PERFORMANCE')
|
||||||
|
|
||||||
|
def social_interaction(self, user: str, action: str, target: str, target_type: str = 'post', target_user: str = None):
|
||||||
|
"""Erweiterte Social Media Interaktion Logging"""
|
||||||
|
message = f"{user} {action} {target_type}"
|
||||||
|
if target_user and target_user != user:
|
||||||
|
message += f" von {target_user}"
|
||||||
|
message += f" (ID: {target})"
|
||||||
|
|
||||||
|
self.info(message, 'SOCIAL', user=user)
|
||||||
|
|
||||||
|
def system_startup(self, version: str = None, environment: str = None, port: int = None):
|
||||||
|
"""Erweiterte System-Start Logging"""
|
||||||
|
message = "🚀 SysTades Social Network gestartet"
|
||||||
|
if version:
|
||||||
|
message += f" (v{version})"
|
||||||
|
if environment:
|
||||||
|
message += f" in {environment} Umgebung"
|
||||||
|
if port:
|
||||||
|
message += f" auf Port {port}"
|
||||||
|
self.info(message, 'SYSTEM')
|
||||||
|
|
||||||
|
def system_shutdown(self, reason: str = None, uptime: float = None):
|
||||||
|
"""Erweiterte System-Shutdown Logging"""
|
||||||
|
message = "🛑 SysTades Social Network beendet"
|
||||||
|
if uptime:
|
||||||
|
message += f" (Laufzeit: {self._format_duration(uptime)})"
|
||||||
|
if reason:
|
||||||
|
message += f" - Grund: {reason}"
|
||||||
|
self.info(message, 'SYSTEM')
|
||||||
|
|
||||||
|
def file_operation(self, operation: str, filename: str, success: bool = True, size: int = None, user: str = None):
|
||||||
|
"""Datei-Operation Logging"""
|
||||||
|
message = f"File {operation.upper()}: {filename}"
|
||||||
|
if size:
|
||||||
|
message += f" ({self._format_bytes(size)})"
|
||||||
|
|
||||||
|
extras = {}
|
||||||
|
if user:
|
||||||
|
extras['user'] = user
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.info(message, 'SYSTEM', **extras)
|
||||||
|
else:
|
||||||
|
self.error(message, 'SYSTEM', **extras)
|
||||||
|
|
||||||
|
def cache_operation(self, operation: str, key: str, hit: bool = None, size: int = None):
|
||||||
|
"""Cache-Operation Logging"""
|
||||||
|
message = f"Cache {operation.upper()}: {key}"
|
||||||
|
if hit is not None:
|
||||||
|
message += f" ({'HIT' if hit else 'MISS'})"
|
||||||
|
if size:
|
||||||
|
message += f" ({self._format_bytes(size)})"
|
||||||
|
|
||||||
|
self.debug(message, 'SYSTEM')
|
||||||
|
|
||||||
|
def email_sent(self, recipient: str, subject: str, success: bool = True, error: str = None):
|
||||||
|
"""E-Mail Versand Logging"""
|
||||||
|
message = f"E-Mail an {recipient}: '{subject}'"
|
||||||
|
if not success and error:
|
||||||
|
message += f" - Fehler: {error}"
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.info(message, 'SYSTEM')
|
||||||
|
else:
|
||||||
|
self.error(message, 'SYSTEM')
|
||||||
|
|
||||||
|
def _format_bytes(self, bytes_count: int) -> str:
|
||||||
|
"""Formatiert Byte-Anzahl in lesbare Form"""
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||||
|
if bytes_count < 1024.0:
|
||||||
|
return f"{bytes_count:.1f}{unit}"
|
||||||
|
bytes_count /= 1024.0
|
||||||
|
return f"{bytes_count:.1f}TB"
|
||||||
|
|
||||||
|
def _format_duration(self, seconds: float) -> str:
|
||||||
|
"""Formatiert Dauer in lesbare Form"""
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.1f}s"
|
||||||
|
elif seconds < 3600:
|
||||||
|
return f"{seconds/60:.1f}min"
|
||||||
|
else:
|
||||||
|
return f"{seconds/3600:.1f}h"
|
||||||
|
|
||||||
|
def exception(self, exc: Exception, context: str = None, user: str = None):
|
||||||
|
"""Erweiterte Exception Logging mit mehr Details"""
|
||||||
|
message = f"Exception: {type(exc).__name__}: {str(exc)}"
|
||||||
|
if context:
|
||||||
|
message = f"{context} - {message}"
|
||||||
|
|
||||||
|
# Stack-Trace hinzufügen
|
||||||
|
stack_trace = traceback.format_exc()
|
||||||
|
message += f"\n{stack_trace}"
|
||||||
|
|
||||||
|
extras = {}
|
||||||
|
if user:
|
||||||
|
extras['user'] = user
|
||||||
|
|
||||||
|
self.error(message, 'ERROR', **extras)
|
||||||
|
|
||||||
|
def log_execution_time(component: str = 'SYSTEM'):
|
||||||
|
"""Decorator für Ausführungszeit-Logging"""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
logger = SocialNetworkLogger()
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
execution_time = (time.time() - start_time) * 1000
|
||||||
|
logger.performance_metric(f"{func.__name__} Ausführungszeit", execution_time, 'ms')
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
execution_time = (time.time() - start_time) * 1000
|
||||||
|
logger.exception(e, f"Fehler in {func.__name__} nach {execution_time:.2f}ms")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def log_api_call(func):
|
||||||
|
"""Decorator für API-Call Logging"""
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
from flask import request, current_user
|
||||||
|
|
||||||
|
logger = SocialNetworkLogger()
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Request-Informationen sammeln
|
||||||
|
method = request.method
|
||||||
|
endpoint = request.endpoint or request.path
|
||||||
|
user = current_user.username if hasattr(current_user, 'username') and current_user.is_authenticated else 'Anonymous'
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# Status-Code ermitteln
|
||||||
|
status = getattr(result, 'status_code', 200) if hasattr(result, 'status_code') else 200
|
||||||
|
|
||||||
|
logger.api_request(method, endpoint, user, status, duration)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
logger.api_request(method, endpoint, user, 500, duration)
|
||||||
|
logger.exception(e, f"API-Fehler in {endpoint}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def performance_monitor(operation_name: str = None):
|
||||||
|
"""Erweiterte Decorator für Performance-Monitoring mit schönen Logs"""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
logger = SocialNetworkLogger()
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
op_name = operation_name or func.__name__
|
||||||
|
|
||||||
|
# User-Info ermitteln falls verfügbar
|
||||||
|
user = None
|
||||||
|
try:
|
||||||
|
from flask import current_user
|
||||||
|
if hasattr(current_user, 'username') and current_user.is_authenticated:
|
||||||
|
user = current_user.username
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
duration = (time.time() - start_time) * 1000
|
||||||
|
|
||||||
|
# Performance-Kategorisierung mit Emojis
|
||||||
|
if duration > 2000:
|
||||||
|
logger.critical(f"Kritisch langsame Operation: {op_name}", 'PERFORMANCE',
|
||||||
|
user=user, duration=duration)
|
||||||
|
elif duration > 1000:
|
||||||
|
logger.warning(f"Langsame Operation: {op_name}", 'PERFORMANCE',
|
||||||
|
user=user, duration=duration)
|
||||||
|
elif duration > 500:
|
||||||
|
logger.info(f"Mäßige Operation: {op_name}", 'PERFORMANCE',
|
||||||
|
user=user, duration=duration)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Schnelle Operation: {op_name}", 'PERFORMANCE',
|
||||||
|
user=user, duration=duration)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
duration = (time.time() - start_time) * 1000
|
||||||
|
logger.error(f"Fehler in Operation: {op_name} nach {duration:.2f}ms", 'PERFORMANCE',
|
||||||
|
user=user, duration=duration)
|
||||||
|
logger.exception(e, f"Performance Monitor - {op_name}", user=user)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def log_user_activity(activity_name: str):
|
||||||
|
"""Erweiterte Decorator für User-Activity Logging mit Details"""
|
||||||
|
def decorator(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
from flask import current_user, request
|
||||||
|
|
||||||
|
logger = SocialNetworkLogger()
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# User und Request-Info sammeln
|
||||||
|
username = 'Anonymous'
|
||||||
|
ip = None
|
||||||
|
user_agent = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(current_user, 'username') and current_user.is_authenticated:
|
||||||
|
username = current_user.username
|
||||||
|
if request:
|
||||||
|
ip = request.remote_addr
|
||||||
|
user_agent = str(request.user_agent)[:100] # Begrenzen
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
duration = (time.time() - start_time) * 1000
|
||||||
|
|
||||||
|
# Erfolgreiche Aktivität loggen
|
||||||
|
details = f"Erfolgreich in {duration:.2f}ms"
|
||||||
|
if user_agent:
|
||||||
|
details += f" (Browser: {user_agent.split('/')[0] if '/' in user_agent else user_agent})"
|
||||||
|
|
||||||
|
logger.user_action(username, activity_name, details=details)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
duration = (time.time() - start_time) * 1000
|
||||||
|
logger.error(f"Fehler in User-Activity '{activity_name}' für {username} nach {duration:.2f}ms: {str(e)}",
|
||||||
|
'ACTIVITY', user=username, ip=ip, duration=duration)
|
||||||
|
logger.exception(e, f"User Activity - {activity_name}", user=username)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
# Globale Logger-Instanz
|
||||||
|
social_logger = SocialNetworkLogger()
|
||||||
|
|
||||||
|
def get_logger(name: str = None) -> SocialNetworkLogger:
|
||||||
|
"""Factory-Funktion für Logger-Instanzen"""
|
||||||
|
if name:
|
||||||
|
return SocialNetworkLogger(name)
|
||||||
|
return social_logger
|
||||||
|
|
||||||
|
# Convenience-Funktionen für häufige Log-Operationen
|
||||||
|
def log_user_login(username: str, ip: str = None, success: bool = True):
|
||||||
|
"""Shortcut für Login-Logging"""
|
||||||
|
if success:
|
||||||
|
social_logger.auth_success(username, ip)
|
||||||
|
else:
|
||||||
|
social_logger.auth_failure(username, ip)
|
||||||
|
|
||||||
|
def log_user_action(username: str, action: str, details: str = None):
|
||||||
|
"""Shortcut für Benutzer-Aktionen"""
|
||||||
|
social_logger.user_action(username, action, details)
|
||||||
|
|
||||||
|
def log_social_action(user: str, action: str, target: str, target_type: str = 'post'):
|
||||||
|
"""Shortcut für Social Media Aktionen"""
|
||||||
|
social_logger.social_interaction(user, action, target, target_type)
|
||||||
|
|
||||||
|
def log_error(message: str, exception: Exception = None):
|
||||||
|
"""Shortcut für Error-Logging"""
|
||||||
|
if exception:
|
||||||
|
social_logger.exception(exception, message)
|
||||||
|
else:
|
||||||
|
social_logger.error(message)
|
||||||
|
|
||||||
|
def log_performance(metric_name: str, value: float, unit: str = 'ms'):
|
||||||
|
"""Shortcut für Performance-Logging"""
|
||||||
|
social_logger.performance_metric(metric_name, value, unit)
|
||||||
|
|
||||||
|
# Setup-Funktion für initiale Konfiguration
|
||||||
|
def setup_logging(app=None, log_level: str = 'INFO'):
|
||||||
|
"""Setup-Funktion für die Flask-App"""
|
||||||
|
if app:
|
||||||
|
# Flask App Logging konfigurieren
|
||||||
|
app.logger.handlers.clear()
|
||||||
|
app.logger.addHandler(social_logger.logger.handlers[0]) # Console Handler
|
||||||
|
app.logger.setLevel(getattr(logging, log_level.upper()))
|
||||||
|
|
||||||
|
# System-Start loggen
|
||||||
|
social_logger.system_startup()
|
||||||
|
|
||||||
|
return social_logger
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Test des Logging-Systems
|
||||||
|
logger = SocialNetworkLogger()
|
||||||
|
|
||||||
|
logger.info("🧪 Teste das Logging-System")
|
||||||
|
logger.auth_success("testuser", "192.168.1.1")
|
||||||
|
logger.user_action("testuser", "Post erstellt", "Neuer Gedanke geteilt")
|
||||||
|
logger.social_interaction("user1", "like", "post_123")
|
||||||
|
logger.api_request("GET", "/api/social/posts", "testuser", 200, 0.045)
|
||||||
|
logger.database_operation("INSERT", "social_posts", True, "Neuer Post gespeichert")
|
||||||
|
logger.performance_metric("Seitenladezeit", 1234.5)
|
||||||
|
logger.warning("⚠️ Test-Warnung")
|
||||||
|
logger.error("❌ Test-Fehler")
|
||||||
|
logger.debug("🔍 Debug-Information")
|
||||||
|
|
||||||
|
print(f"\n{Colors.BRIGHT_GREEN}✅ Logging-System erfolgreich getestet!{Colors.RESET}")
|
||||||
|
print(f"{Colors.CYAN}📁 Logs gespeichert in: {LoggerConfig.LOG_DIR}/{Colors.RESET}")
|
||||||
65
utils/update_db.py
Normal file
65
utils/update_db.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Bestimme den absoluten Pfad zur Datenbank
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
db_path = os.path.join(basedir, 'database', 'systades.db')
|
||||||
|
|
||||||
|
def update_user_table():
|
||||||
|
"""Aktualisiert die User-Tabelle mit den fehlenden Spalten"""
|
||||||
|
|
||||||
|
# Überprüfe, ob die Datenbankdatei existiert
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print(f"Datenbank nicht gefunden unter: {db_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verbindung zur Datenbank herstellen
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Überprüfe, ob die neuen Spalten bereits existieren
|
||||||
|
cursor.execute("PRAGMA table_info(user)")
|
||||||
|
columns = [info[1] for info in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Neue Spalten, die hinzugefügt werden müssen
|
||||||
|
new_columns = {
|
||||||
|
'bio': 'TEXT',
|
||||||
|
'location': 'VARCHAR(100)',
|
||||||
|
'website': 'VARCHAR(200)',
|
||||||
|
'avatar': 'VARCHAR(200)',
|
||||||
|
'last_login': 'DATETIME'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spalten hinzufügen, die noch nicht existieren
|
||||||
|
for col_name, col_type in new_columns.items():
|
||||||
|
if col_name not in columns:
|
||||||
|
print(f"Füge Spalte '{col_name}' zur User-Tabelle hinzu...")
|
||||||
|
cursor.execute(f"ALTER TABLE user ADD COLUMN {col_name} {col_type}")
|
||||||
|
|
||||||
|
# Änderungen speichern
|
||||||
|
conn.commit()
|
||||||
|
print("User-Tabelle erfolgreich aktualisiert!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Fehler bei der Datenbankaktualisierung: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Führe die Aktualisierung durch
|
||||||
|
success = update_user_table()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("Die Datenbank wurde erfolgreich aktualisiert.")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("Es gab ein Problem bei der Datenbankaktualisierung.")
|
||||||
|
sys.exit(1)
|
||||||
BIN
utils/update_routes.py
Normal file
BIN
utils/update_routes.py
Normal file
Binary file not shown.
@@ -9,11 +9,22 @@ from datetime import datetime
|
|||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
from app import app
|
# Import models direkt, app wird lazy geladen
|
||||||
from models import db, User
|
from models import db, User
|
||||||
|
|
||||||
|
def get_app():
|
||||||
|
"""Lazy loading der Flask app um zirkuläre Imports zu vermeiden"""
|
||||||
|
try:
|
||||||
|
from flask import current_app
|
||||||
|
return current_app
|
||||||
|
except RuntimeError:
|
||||||
|
# Fallback wenn kein app context existiert
|
||||||
|
from app import app
|
||||||
|
return app
|
||||||
|
|
||||||
def list_users():
|
def list_users():
|
||||||
"""List all users in the database."""
|
"""List all users in the database."""
|
||||||
|
app = get_app()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
@@ -37,6 +48,7 @@ def list_users():
|
|||||||
|
|
||||||
def create_user(username, email, password, is_admin=False):
|
def create_user(username, email, password, is_admin=False):
|
||||||
"""Create a new user in the database."""
|
"""Create a new user in the database."""
|
||||||
|
app = get_app()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
# Check if user already exists
|
# Check if user already exists
|
||||||
@@ -55,7 +67,7 @@ def create_user(username, email, password, is_admin=False):
|
|||||||
user = User(
|
user = User(
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
is_admin=is_admin,
|
role='admin' if is_admin else 'user',
|
||||||
created_at=datetime.utcnow()
|
created_at=datetime.utcnow()
|
||||||
)
|
)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
@@ -73,6 +85,7 @@ def create_user(username, email, password, is_admin=False):
|
|||||||
|
|
||||||
def reset_password(username, new_password):
|
def reset_password(username, new_password):
|
||||||
"""Reset password for a user."""
|
"""Reset password for a user."""
|
||||||
|
app = get_app()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
@@ -93,6 +106,7 @@ def reset_password(username, new_password):
|
|||||||
|
|
||||||
def delete_user(username):
|
def delete_user(username):
|
||||||
"""Delete a user from the database."""
|
"""Delete a user from the database."""
|
||||||
|
app = get_app()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).first()
|
||||||
|
|||||||
Reference in New Issue
Block a user