✨ feat: Implementierung von Benachrichtigungen und sozialen Funktionen; Hinzufügen von API-Endpunkten für Benachrichtigungen, Benutzer-Follows und soziale Interaktionen; Verbesserung des Logging-Systems zur besseren Nachverfolgbarkeit von Systemereignissen.
This commit is contained in:
@@ -307,7 +307,7 @@
|
||||
.chat-assistant .chat-messages {
|
||||
max-height: calc(80vh - 160px) !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="{{ request.endpoint }}" class="relative overflow-x-hidden dark bg-gray-900 text-white" x-data="{
|
||||
darkMode: true,
|
||||
@@ -404,6 +404,22 @@
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'mindmap' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-diagram-project mr-2"></i>Mindmap
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('social_feed') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'social_feed' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'social_feed' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-home mr-2"></i>Feed
|
||||
</a>
|
||||
<a href="{{ url_for('discover') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'nav-link-active' if request.endpoint == 'discover' else '' }}'
|
||||
: '{{ 'nav-link-light-active' if request.endpoint == 'discover' else 'nav-link-light' }}'">
|
||||
<i class="fa-solid fa-compass mr-2"></i>Entdecken
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('search_thoughts_page') }}"
|
||||
class="nav-link flex items-center"
|
||||
x-bind:class="darkMode
|
||||
@@ -573,6 +589,22 @@
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'mindmap' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-diagram-project w-5 mr-3"></i>Mindmap
|
||||
</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('social_feed') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'social_feed' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'social_feed' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-home w-5 mr-3"></i>Feed
|
||||
</a>
|
||||
<a href="{{ url_for('discover') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
? '{{ 'bg-purple-500/20 text-white' if request.endpoint == 'discover' else 'text-white/80 hover:bg-gray-800/80 hover:text-white' }}'
|
||||
: '{{ 'bg-purple-500/10 text-gray-900' if request.endpoint == 'discover' else 'text-gray-700 hover:bg-gray-100 hover:text-gray-900' }}'">
|
||||
<i class="fa-solid fa-compass w-5 mr-3"></i>Entdecken
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('search_thoughts_page') }}"
|
||||
class="block py-3.5 px-4 rounded-xl transition-all duration-200 flex items-center"
|
||||
x-bind:class="darkMode
|
||||
|
||||
@@ -411,17 +411,49 @@
|
||||
<div class="mindmap-container">
|
||||
<div id="cy"></div>
|
||||
|
||||
<!-- Zoom-Toolbar für Hauptmindmap -->
|
||||
<!-- Toolbar -->
|
||||
<div class="mindmap-toolbar">
|
||||
<button id="zoomIn" title="Vergrößern">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
<button id="zoomOut" title="Verkleinern">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
<button id="resetView" title="Ansicht zurücksetzen">
|
||||
<i class="fas fa-expand"></i>
|
||||
</button>
|
||||
<div class="toolbar-section">
|
||||
<button id="add-node-btn" class="toolbar-btn" title="Knoten hinzufügen">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>Knoten</span>
|
||||
</button>
|
||||
<button id="add-thought-btn" class="toolbar-btn" title="Gedanken hinzufügen">
|
||||
<i class="fas fa-lightbulb"></i>
|
||||
<span>Gedanke</span>
|
||||
</button>
|
||||
<button id="collaborate-btn" class="toolbar-btn" title="Kollaboration starten">
|
||||
<i class="fas fa-users"></i>
|
||||
<span>Kollaboration</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<button id="export-btn" class="toolbar-btn" title="Mindmap exportieren">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>Export</span>
|
||||
</button>
|
||||
<button id="share-btn" class="toolbar-btn" title="Mindmap teilen">
|
||||
<i class="fas fa-share"></i>
|
||||
<span>Teilen</span>
|
||||
</button>
|
||||
<button id="fullscreen-btn" class="toolbar-btn" title="Vollbild">
|
||||
<i class="fas fa-expand"></i>
|
||||
<span>Vollbild</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<button id="zoom-in-btn" class="toolbar-btn" title="Vergrößern">
|
||||
<i class="fas fa-search-plus"></i>
|
||||
</button>
|
||||
<button id="zoom-out-btn" class="toolbar-btn" title="Verkleinern">
|
||||
<i class="fas fa-search-minus"></i>
|
||||
</button>
|
||||
<button id="reset-view-btn" class="toolbar-btn" title="Ansicht zurücksetzen">
|
||||
<i class="fas fa-home"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mindmap-header">
|
||||
@@ -679,9 +711,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Funktionen für Zoom-Buttons und Reset
|
||||
const zoomInBtn = document.getElementById('zoomIn');
|
||||
const zoomOutBtn = document.getElementById('zoomOut');
|
||||
const resetViewBtn = document.getElementById('resetView');
|
||||
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||
const resetViewBtn = document.getElementById('reset-view-btn');
|
||||
|
||||
if (zoomInBtn && window.cy) {
|
||||
zoomInBtn.addEventListener('click', function() {
|
||||
@@ -700,6 +732,199 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.cy) window.cy.fit();
|
||||
});
|
||||
}
|
||||
|
||||
// Neue Toolbar-Funktionen
|
||||
const addNodeBtn = document.getElementById('add-node-btn');
|
||||
const addThoughtBtn = document.getElementById('add-thought-btn');
|
||||
const collaborateBtn = document.getElementById('collaborate-btn');
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
const shareBtn = document.getElementById('share-btn');
|
||||
const fullscreenBtn = document.getElementById('fullscreen-btn');
|
||||
|
||||
if (addNodeBtn) {
|
||||
addNodeBtn.addEventListener('click', function() {
|
||||
// Öffne Modal zum Hinzufügen eines neuen Knotens
|
||||
showAddNodeModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (addThoughtBtn) {
|
||||
addThoughtBtn.addEventListener('click', function() {
|
||||
// Öffne Modal zum Hinzufügen eines Gedankens
|
||||
showAddThoughtModal();
|
||||
});
|
||||
}
|
||||
|
||||
if (collaborateBtn) {
|
||||
collaborateBtn.addEventListener('click', function() {
|
||||
// Starte Kollaborationsmodus
|
||||
startCollaboration();
|
||||
});
|
||||
}
|
||||
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', function() {
|
||||
// Exportiere Mindmap
|
||||
exportMindmap();
|
||||
});
|
||||
}
|
||||
|
||||
if (shareBtn) {
|
||||
shareBtn.addEventListener('click', function() {
|
||||
// Teile Mindmap
|
||||
shareMindmap();
|
||||
});
|
||||
}
|
||||
|
||||
if (fullscreenBtn) {
|
||||
fullscreenBtn.addEventListener('click', function() {
|
||||
// Vollbild-Modus
|
||||
toggleFullscreen();
|
||||
});
|
||||
}
|
||||
|
||||
// Funktionen implementieren
|
||||
function showAddNodeModal() {
|
||||
// Erstelle ein einfaches Modal für neuen Knoten
|
||||
const nodeName = prompt('Name des neuen Knotens:');
|
||||
if (nodeName && nodeName.trim()) {
|
||||
addNewNode(nodeName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
function showAddThoughtModal() {
|
||||
// Erstelle ein Modal für neuen Gedanken
|
||||
const thoughtTitle = prompt('Titel des Gedankens:');
|
||||
if (thoughtTitle && thoughtTitle.trim()) {
|
||||
addNewThought(thoughtTitle.trim());
|
||||
}
|
||||
}
|
||||
|
||||
function addNewNode(name) {
|
||||
// API-Aufruf zum Hinzufügen eines neuen Knotens
|
||||
fetch('/api/mindmap/public/add_node', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
description: '',
|
||||
x_position: Math.random() * 400 + 100,
|
||||
y_position: Math.random() * 400 + 100
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Lade Mindmap neu
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Hinzufügen des Knotens: ' + (data.error || 'Unbekannter Fehler'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler:', error);
|
||||
alert('Ein Fehler ist aufgetreten.');
|
||||
});
|
||||
}
|
||||
|
||||
function addNewThought(title) {
|
||||
// API-Aufruf zum Hinzufügen eines neuen Gedankens
|
||||
fetch('/api/thoughts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
content: 'Neuer Gedanke erstellt über die Mindmap',
|
||||
branch: 'Allgemein'
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Gedanke erfolgreich erstellt!');
|
||||
} else {
|
||||
alert('Fehler beim Erstellen des Gedankens: ' + (data.error || 'Unbekannter Fehler'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fehler:', error);
|
||||
alert('Ein Fehler ist aufgetreten.');
|
||||
});
|
||||
}
|
||||
|
||||
function startCollaboration() {
|
||||
// Kollaborationsmodus starten
|
||||
alert('Kollaborationsmodus wird bald verfügbar sein!\n\nGeplante Features:\n- Echtzeit-Bearbeitung\n- Live-Cursor anderer Benutzer\n- Chat-Integration\n- Änderungshistorie');
|
||||
}
|
||||
|
||||
function exportMindmap() {
|
||||
// Mindmap exportieren
|
||||
const format = prompt('Export-Format wählen:\n1. JSON\n2. PNG (geplant)\n3. PDF (geplant)\n\nGeben Sie 1, 2 oder 3 ein:', '1');
|
||||
|
||||
if (format === '1') {
|
||||
// JSON-Export
|
||||
if (window.cy) {
|
||||
const data = window.cy.json();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'mindmap-export.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} else {
|
||||
alert('Dieses Format wird bald verfügbar sein!');
|
||||
}
|
||||
}
|
||||
|
||||
function shareMindmap() {
|
||||
// Mindmap teilen
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: 'SysTades Mindmap',
|
||||
text: 'Schau dir diese interessante Mindmap an!',
|
||||
url: window.location.href
|
||||
});
|
||||
} else {
|
||||
// Fallback: URL kopieren
|
||||
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||
alert('Mindmap-Link wurde in die Zwischenablage kopiert!');
|
||||
}).catch(() => {
|
||||
prompt('Kopiere diesen Link zum Teilen:', window.location.href);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
// Vollbild-Modus umschalten
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(err => {
|
||||
console.error('Fehler beim Aktivieren des Vollbildmodus:', err);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// Vollbild-Event-Listener
|
||||
document.addEventListener('fullscreenchange', function() {
|
||||
const fullscreenBtn = document.getElementById('fullscreen-btn');
|
||||
if (fullscreenBtn) {
|
||||
const icon = fullscreenBtn.querySelector('i');
|
||||
if (document.fullscreenElement) {
|
||||
icon.className = 'fas fa-compress';
|
||||
fullscreenBtn.title = 'Vollbild verlassen';
|
||||
} else {
|
||||
icon.className = 'fas fa-expand';
|
||||
fullscreenBtn.title = 'Vollbild';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user