✨ 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:
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 %}
|
||||
Reference in New Issue
Block a user