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:
2025-05-28 22:08:56 +02:00
parent 1f4394e9b6
commit f5c2e70a11
31 changed files with 9294 additions and 591 deletions

View File

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

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

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

View File

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

View File

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