1133 lines
38 KiB
JavaScript
1133 lines
38 KiB
JavaScript
/**
|
|
* SysTades Social Network JavaScript
|
|
* Modernes, performantes JavaScript für Social Features
|
|
*/
|
|
|
|
// =============================================================================
|
|
// UTILITIES & HELPERS
|
|
// =============================================================================
|
|
|
|
class Utils {
|
|
static debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func.apply(this, args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
static throttle(func, limit) {
|
|
let inThrottle;
|
|
return function(...args) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
}
|
|
|
|
static formatTimeAgo(dateString) {
|
|
const now = new Date();
|
|
const date = new Date(dateString);
|
|
const seconds = Math.floor((now - date) / 1000);
|
|
|
|
const intervals = {
|
|
Jahr: 31536000,
|
|
Monat: 2592000,
|
|
Woche: 604800,
|
|
Tag: 86400,
|
|
Stunde: 3600,
|
|
Minute: 60
|
|
};
|
|
|
|
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
|
|
const interval = Math.floor(seconds / secondsInUnit);
|
|
if (interval >= 1) {
|
|
return `vor ${interval} ${unit}${interval !== 1 ? (unit === 'Monat' ? 'en' : unit === 'Jahr' ? 'en' : 'n') : ''}`;
|
|
}
|
|
}
|
|
return 'gerade eben';
|
|
}
|
|
|
|
static escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
static showToast(message, type = 'info', duration = 3000) {
|
|
const toastContainer = document.querySelector('.toast-container') || this.createToastContainer();
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast ${type} fade-in`;
|
|
toast.innerHTML = `
|
|
<div class="flex items-center gap-3">
|
|
<i class="fas fa-${this.getToastIcon(type)} text-lg"></i>
|
|
<span>${this.escapeHtml(message)}</span>
|
|
</div>
|
|
`;
|
|
|
|
toastContainer.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.opacity = '0';
|
|
toast.style.transform = 'translateX(100%)';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, duration);
|
|
}
|
|
|
|
static createToastContainer() {
|
|
const container = document.createElement('div');
|
|
container.className = 'toast-container';
|
|
document.body.appendChild(container);
|
|
return container;
|
|
}
|
|
|
|
static getToastIcon(type) {
|
|
const icons = {
|
|
success: 'check-circle',
|
|
error: 'exclamation-circle',
|
|
warning: 'exclamation-triangle',
|
|
info: 'info-circle'
|
|
};
|
|
return icons[type] || 'info-circle';
|
|
}
|
|
|
|
static async fetchAPI(url, options = {}) {
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
...options.headers
|
|
},
|
|
...options
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
static animateCount(element, targetValue, duration = 1000) {
|
|
const startValue = parseInt(element.textContent) || 0;
|
|
const difference = targetValue - startValue;
|
|
const increment = difference / (duration / 16);
|
|
let currentValue = startValue;
|
|
|
|
const updateCount = () => {
|
|
currentValue += increment;
|
|
if ((increment > 0 && currentValue >= targetValue) ||
|
|
(increment < 0 && currentValue <= targetValue)) {
|
|
element.textContent = targetValue;
|
|
return;
|
|
}
|
|
element.textContent = Math.floor(currentValue);
|
|
requestAnimationFrame(updateCount);
|
|
};
|
|
|
|
requestAnimationFrame(updateCount);
|
|
}
|
|
|
|
static createLoadingSpinner() {
|
|
const spinner = document.createElement('div');
|
|
spinner.className = 'loading-spinner';
|
|
return spinner;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SOCIAL FEED CLASS
|
|
// =============================================================================
|
|
|
|
class SocialFeed {
|
|
constructor() {
|
|
this.currentFilter = 'public';
|
|
this.isLoading = false;
|
|
this.page = 1;
|
|
this.hasMore = true;
|
|
this.posts = new Map();
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.bindEvents();
|
|
this.loadPosts();
|
|
this.setupIntersectionObserver();
|
|
}
|
|
|
|
bindEvents() {
|
|
// Create post form
|
|
const createForm = document.getElementById('create-post-form');
|
|
if (createForm) {
|
|
createForm.addEventListener('submit', this.handleCreatePost.bind(this));
|
|
}
|
|
|
|
// Filter tabs
|
|
document.querySelectorAll('.filter-tab').forEach(tab => {
|
|
tab.addEventListener('click', this.handleFilterChange.bind(this));
|
|
});
|
|
|
|
// Global event delegation for post actions
|
|
document.addEventListener('click', this.handlePostAction.bind(this));
|
|
document.addEventListener('submit', this.handleCommentSubmit.bind(this));
|
|
}
|
|
|
|
setupIntersectionObserver() {
|
|
const loadMoreTrigger = document.getElementById('load-more-trigger');
|
|
if (!loadMoreTrigger) return;
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
if (entries[0].isIntersecting && !this.isLoading && this.hasMore) {
|
|
this.loadMorePosts();
|
|
}
|
|
}, { threshold: 0.1 });
|
|
|
|
observer.observe(loadMoreTrigger);
|
|
}
|
|
|
|
async handleCreatePost(event) {
|
|
event.preventDefault();
|
|
|
|
const form = event.target;
|
|
const formData = new FormData(form);
|
|
const content = formData.get('content')?.trim();
|
|
|
|
if (!content) {
|
|
Utils.showToast('Bitte geben Sie Inhalt für den Post ein', 'warning');
|
|
return;
|
|
}
|
|
|
|
const submitBtn = form.querySelector('.create-post-btn');
|
|
const originalText = submitBtn.textContent;
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Wird erstellt...';
|
|
|
|
try {
|
|
const postData = {
|
|
content: content,
|
|
post_type: formData.get('post_type') || 'text',
|
|
visibility: formData.get('visibility') || 'public'
|
|
};
|
|
|
|
const response = await Utils.fetchAPI('/api/social/posts', {
|
|
method: 'POST',
|
|
body: JSON.stringify(postData)
|
|
});
|
|
|
|
if (response.success) {
|
|
Utils.showToast('Post erfolgreich erstellt!', 'success');
|
|
form.reset();
|
|
this.refreshFeed();
|
|
} else {
|
|
throw new Error(response.error || 'Fehler beim Erstellen des Posts');
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast(error.message, 'error');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = originalText;
|
|
}
|
|
}
|
|
|
|
handleFilterChange(event) {
|
|
const newFilter = event.target.dataset.filter;
|
|
if (newFilter === this.currentFilter) return;
|
|
|
|
// Update UI
|
|
document.querySelectorAll('.filter-tab').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
event.target.classList.add('active');
|
|
|
|
// Reset pagination and reload
|
|
this.currentFilter = newFilter;
|
|
this.page = 1;
|
|
this.hasMore = true;
|
|
this.posts.clear();
|
|
|
|
const postsContainer = document.getElementById('posts-container');
|
|
if (postsContainer) {
|
|
postsContainer.innerHTML = '';
|
|
}
|
|
|
|
this.loadPosts();
|
|
}
|
|
|
|
async loadPosts() {
|
|
if (this.isLoading) return;
|
|
this.isLoading = true;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
filter: this.currentFilter,
|
|
page: this.page,
|
|
per_page: 10
|
|
});
|
|
|
|
const response = await Utils.fetchAPI(`/api/social/posts?${params}`);
|
|
|
|
if (response.success) {
|
|
this.renderPosts(response.posts);
|
|
this.hasMore = response.has_more;
|
|
this.page++;
|
|
} else {
|
|
throw new Error(response.error || 'Fehler beim Laden der Posts');
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Laden der Posts', 'error');
|
|
console.error('Load posts error:', error);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
async loadMorePosts() {
|
|
await this.loadPosts();
|
|
}
|
|
|
|
renderPosts(posts) {
|
|
const container = document.getElementById('posts-container');
|
|
if (!container) return;
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
posts.forEach(post => {
|
|
if (!this.posts.has(post.id)) {
|
|
this.posts.set(post.id, post);
|
|
const postElement = this.createPostElement(post);
|
|
fragment.appendChild(postElement);
|
|
}
|
|
});
|
|
|
|
container.appendChild(fragment);
|
|
|
|
// Animate new posts
|
|
const newPosts = container.querySelectorAll('.post-card:not(.animated)');
|
|
newPosts.forEach((post, index) => {
|
|
post.classList.add('animated');
|
|
setTimeout(() => {
|
|
post.classList.add('fade-in');
|
|
}, index * 100);
|
|
});
|
|
}
|
|
|
|
createPostElement(post) {
|
|
const postDiv = document.createElement('div');
|
|
postDiv.className = 'post-card';
|
|
postDiv.dataset.postId = post.id;
|
|
|
|
postDiv.innerHTML = `
|
|
<div class="post-header">
|
|
<img src="${post.author.avatar_url || '/static/img/default-avatar.jpg'}"
|
|
alt="${Utils.escapeHtml(post.author.display_name)}"
|
|
class="post-avatar"
|
|
onclick="window.location.href='/profile/${post.author.username}'">
|
|
<div class="post-author">
|
|
<h4 class="post-author-name">${Utils.escapeHtml(post.author.display_name)}</h4>
|
|
<p class="post-author-username">@${Utils.escapeHtml(post.author.username)}</p>
|
|
</div>
|
|
<span class="post-time">${Utils.formatTimeAgo(post.created_at)}</span>
|
|
</div>
|
|
|
|
${post.post_type !== 'text' ? `
|
|
<span class="post-type-badge post-type-${post.post_type}">
|
|
${this.getPostTypeLabel(post.post_type)}
|
|
</span>
|
|
` : ''}
|
|
|
|
<div class="post-content">
|
|
${this.formatPostContent(post.content)}
|
|
</div>
|
|
|
|
<div class="post-actions">
|
|
<div class="action-group">
|
|
<button class="action-btn like-btn ${post.user_liked ? 'active' : ''}"
|
|
data-action="like" data-post-id="${post.id}">
|
|
<i class="fas fa-heart"></i>
|
|
<span class="like-count">${post.like_count}</span>
|
|
</button>
|
|
<button class="action-btn comment-btn"
|
|
data-action="comment" data-post-id="${post.id}">
|
|
<i class="fas fa-comment"></i>
|
|
<span>${post.comment_count}</span>
|
|
</button>
|
|
<button class="action-btn share-btn"
|
|
data-action="share" data-post-id="${post.id}">
|
|
<i class="fas fa-share"></i>
|
|
<span>${post.share_count}</span>
|
|
</button>
|
|
</div>
|
|
<div class="action-group">
|
|
<button class="action-btn bookmark-btn ${post.user_bookmarked ? 'active' : ''}"
|
|
data-action="bookmark" data-post-id="${post.id}">
|
|
<i class="fas fa-bookmark"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comments-section" id="comments-${post.id}" style="display: none;">
|
|
<div class="comments-list"></div>
|
|
<form class="comment-form" data-post-id="${post.id}">
|
|
<textarea placeholder="Kommentar schreiben..." required></textarea>
|
|
<button type="submit" class="comment-submit">
|
|
<i class="fas fa-paper-plane"></i>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
return postDiv;
|
|
}
|
|
|
|
getPostTypeLabel(type) {
|
|
const labels = {
|
|
text: 'Text',
|
|
thought: 'Gedanke',
|
|
question: 'Frage',
|
|
insight: 'Erkenntnis'
|
|
};
|
|
return labels[type] || 'Text';
|
|
}
|
|
|
|
formatPostContent(content) {
|
|
// Basic formatting: links, mentions, hashtags
|
|
return Utils.escapeHtml(content)
|
|
.replace(/\n/g, '<br>')
|
|
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" target="_blank" class="text-blue-600 hover:underline">$1</a>')
|
|
.replace(/@([a-zA-Z0-9_]+)/g, '<a href="/profile/$1" class="text-blue-600 hover:underline">@$1</a>')
|
|
.replace(/#([a-zA-Z0-9_]+)/g, '<span class="text-blue-600 font-medium">#$1</span>');
|
|
}
|
|
|
|
async handlePostAction(event) {
|
|
const actionBtn = event.target.closest('.action-btn');
|
|
if (!actionBtn) return;
|
|
|
|
const action = actionBtn.dataset.action;
|
|
const postId = actionBtn.dataset.postId;
|
|
|
|
if (!action || !postId) return;
|
|
|
|
event.preventDefault();
|
|
|
|
switch (action) {
|
|
case 'like':
|
|
await this.toggleLike(postId, actionBtn);
|
|
break;
|
|
case 'comment':
|
|
this.toggleComments(postId);
|
|
break;
|
|
case 'share':
|
|
await this.sharePost(postId);
|
|
break;
|
|
case 'bookmark':
|
|
await this.toggleBookmark(postId, actionBtn);
|
|
break;
|
|
}
|
|
}
|
|
|
|
async toggleLike(postId, button) {
|
|
try {
|
|
const response = await Utils.fetchAPI(`/api/social/posts/${postId}/like`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
const isLiked = response.liked;
|
|
const likeCount = response.like_count;
|
|
|
|
button.classList.toggle('active', isLiked);
|
|
|
|
const countElement = button.querySelector('.like-count');
|
|
Utils.animateCount(countElement, likeCount);
|
|
|
|
// Add animation effect
|
|
const heart = button.querySelector('i');
|
|
heart.classList.add('animate-pulse');
|
|
setTimeout(() => heart.classList.remove('animate-pulse'), 300);
|
|
|
|
} else {
|
|
throw new Error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Liken des Posts', 'error');
|
|
}
|
|
}
|
|
|
|
toggleComments(postId) {
|
|
const commentsSection = document.getElementById(`comments-${postId}`);
|
|
if (!commentsSection) return;
|
|
|
|
const isVisible = commentsSection.style.display !== 'none';
|
|
|
|
if (isVisible) {
|
|
commentsSection.style.display = 'none';
|
|
} else {
|
|
commentsSection.style.display = 'block';
|
|
this.loadComments(postId);
|
|
}
|
|
}
|
|
|
|
async loadComments(postId) {
|
|
try {
|
|
const response = await Utils.fetchAPI(`/api/social/posts/${postId}/comments`);
|
|
|
|
if (response.success) {
|
|
const commentsList = document.querySelector(`#comments-${postId} .comments-list`);
|
|
if (commentsList) {
|
|
commentsList.innerHTML = response.comments.map(comment => this.createCommentHTML(comment)).join('');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Laden der Kommentare', 'error');
|
|
}
|
|
}
|
|
|
|
createCommentHTML(comment) {
|
|
return `
|
|
<div class="comment-item">
|
|
<img src="${comment.author.avatar_url || '/static/img/default-avatar.jpg'}"
|
|
alt="${Utils.escapeHtml(comment.author.display_name)}"
|
|
class="comment-avatar">
|
|
<div class="comment-content">
|
|
<div class="comment-header">
|
|
<span class="comment-author">${Utils.escapeHtml(comment.author.display_name)}</span>
|
|
<span class="comment-time">${Utils.formatTimeAgo(comment.created_at)}</span>
|
|
</div>
|
|
<p class="comment-text">${this.formatPostContent(comment.content)}</p>
|
|
<div class="comment-actions">
|
|
<button class="comment-action">
|
|
<i class="fas fa-heart mr-1"></i>${comment.like_count}
|
|
</button>
|
|
<button class="comment-action">Antworten</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async handleCommentSubmit(event) {
|
|
if (!event.target.classList.contains('comment-form')) return;
|
|
|
|
event.preventDefault();
|
|
|
|
const form = event.target;
|
|
const postId = form.dataset.postId;
|
|
const textarea = form.querySelector('textarea');
|
|
const content = textarea.value.trim();
|
|
|
|
if (!content) return;
|
|
|
|
const submitBtn = form.querySelector('.comment-submit');
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
|
|
try {
|
|
const response = await Utils.fetchAPI(`/api/social/posts/${postId}/comments`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content })
|
|
});
|
|
|
|
if (response.success) {
|
|
Utils.showToast('Kommentar hinzugefügt!', 'success');
|
|
textarea.value = '';
|
|
this.loadComments(postId);
|
|
|
|
// Update comment count
|
|
const commentBtn = document.querySelector(`[data-action="comment"][data-post-id="${postId}"]`);
|
|
if (commentBtn) {
|
|
const countElement = commentBtn.querySelector('span');
|
|
const currentCount = parseInt(countElement.textContent) || 0;
|
|
Utils.animateCount(countElement, currentCount + 1);
|
|
}
|
|
} else {
|
|
throw new Error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Hinzufügen des Kommentars', 'error');
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
|
}
|
|
}
|
|
|
|
async sharePost(postId) {
|
|
try {
|
|
const response = await Utils.fetchAPI(`/api/social/posts/${postId}/share`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
Utils.showToast('Post geteilt!', 'success');
|
|
|
|
// Update share count
|
|
const shareBtn = document.querySelector(`[data-action="share"][data-post-id="${postId}"]`);
|
|
if (shareBtn) {
|
|
const countElement = shareBtn.querySelector('span');
|
|
const currentCount = parseInt(countElement.textContent) || 0;
|
|
Utils.animateCount(countElement, currentCount + 1);
|
|
}
|
|
} else {
|
|
throw new Error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Teilen des Posts', 'error');
|
|
}
|
|
}
|
|
|
|
async toggleBookmark(postId, button) {
|
|
try {
|
|
const response = await Utils.fetchAPI(`/api/social/posts/${postId}/bookmark`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
const isBookmarked = response.bookmarked;
|
|
button.classList.toggle('active', isBookmarked);
|
|
|
|
Utils.showToast(
|
|
isBookmarked ? 'Post gespeichert!' : 'Speicherung entfernt!',
|
|
'success'
|
|
);
|
|
} else {
|
|
throw new Error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Speichern des Posts', 'error');
|
|
}
|
|
}
|
|
|
|
refreshFeed() {
|
|
this.page = 1;
|
|
this.hasMore = true;
|
|
this.posts.clear();
|
|
|
|
const postsContainer = document.getElementById('posts-container');
|
|
if (postsContainer) {
|
|
postsContainer.innerHTML = '';
|
|
}
|
|
|
|
this.loadPosts();
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// NOTIFICATION CENTER CLASS
|
|
// =============================================================================
|
|
|
|
class NotificationCenter {
|
|
constructor() {
|
|
this.currentFilter = 'all';
|
|
this.page = 1;
|
|
this.hasMore = true;
|
|
this.isLoading = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.bindEvents();
|
|
this.loadNotifications();
|
|
this.pollForNewNotifications();
|
|
}
|
|
|
|
bindEvents() {
|
|
// Filter tabs
|
|
document.querySelectorAll('.notification-filter').forEach(filter => {
|
|
filter.addEventListener('click', this.handleFilterChange.bind(this));
|
|
});
|
|
|
|
// Mark all as read
|
|
const markAllBtn = document.getElementById('mark-all-read');
|
|
if (markAllBtn) {
|
|
markAllBtn.addEventListener('click', this.markAllAsRead.bind(this));
|
|
}
|
|
|
|
// Event delegation for notification actions
|
|
document.addEventListener('click', this.handleNotificationAction.bind(this));
|
|
}
|
|
|
|
handleFilterChange(event) {
|
|
const newFilter = event.target.dataset.filter;
|
|
if (newFilter === this.currentFilter) return;
|
|
|
|
// Update UI
|
|
document.querySelectorAll('.notification-filter').forEach(filter => {
|
|
filter.classList.remove('active');
|
|
});
|
|
event.target.classList.add('active');
|
|
|
|
// Reset and reload
|
|
this.currentFilter = newFilter;
|
|
this.page = 1;
|
|
this.hasMore = true;
|
|
|
|
const container = document.getElementById('notifications-container');
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
}
|
|
|
|
this.loadNotifications();
|
|
}
|
|
|
|
async loadNotifications() {
|
|
if (this.isLoading) return;
|
|
this.isLoading = true;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
filter: this.currentFilter,
|
|
page: this.page,
|
|
per_page: 20
|
|
});
|
|
|
|
const response = await Utils.fetchAPI(`/api/social/notifications?${params}`);
|
|
|
|
if (response.success) {
|
|
this.renderNotifications(response.notifications);
|
|
this.hasMore = response.has_more;
|
|
this.page++;
|
|
this.updateNotificationBadge(response.unread_count);
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Laden der Benachrichtigungen', 'error');
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
renderNotifications(notifications) {
|
|
const container = document.getElementById('notifications-container');
|
|
if (!container) return;
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
notifications.forEach(notification => {
|
|
const notificationElement = this.createNotificationElement(notification);
|
|
fragment.appendChild(notificationElement);
|
|
});
|
|
|
|
container.appendChild(fragment);
|
|
}
|
|
|
|
createNotificationElement(notification) {
|
|
const div = document.createElement('div');
|
|
div.className = `notification-item ${!notification.is_read ? 'unread' : ''}`;
|
|
div.dataset.notificationId = notification.id;
|
|
|
|
div.innerHTML = `
|
|
<div class="notification-icon notification-${notification.type}">
|
|
<i class="fas fa-${this.getNotificationIcon(notification.type)}"></i>
|
|
</div>
|
|
<div class="notification-content">
|
|
<p class="notification-text">${Utils.escapeHtml(notification.message)}</p>
|
|
<span class="notification-time">${Utils.formatTimeAgo(notification.created_at)}</span>
|
|
</div>
|
|
<div class="notification-actions">
|
|
${!notification.is_read ? `
|
|
<button class="notification-mark-read" data-action="mark-read">
|
|
<i class="fas fa-check"></i>
|
|
</button>
|
|
` : ''}
|
|
<button class="notification-delete" data-action="delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
return div;
|
|
}
|
|
|
|
getNotificationIcon(type) {
|
|
const icons = {
|
|
like: 'heart',
|
|
comment: 'comment',
|
|
follow: 'user-plus',
|
|
share: 'share',
|
|
mention: 'at'
|
|
};
|
|
return icons[type] || 'bell';
|
|
}
|
|
|
|
async handleNotificationAction(event) {
|
|
const actionBtn = event.target.closest('[data-action]');
|
|
if (!actionBtn) return;
|
|
|
|
const action = actionBtn.dataset.action;
|
|
const notificationItem = actionBtn.closest('.notification-item');
|
|
const notificationId = notificationItem?.dataset.notificationId;
|
|
|
|
if (!notificationId) return;
|
|
|
|
event.preventDefault();
|
|
|
|
switch (action) {
|
|
case 'mark-read':
|
|
await this.markAsRead(notificationId, notificationItem);
|
|
break;
|
|
case 'delete':
|
|
await this.deleteNotification(notificationId, notificationItem);
|
|
break;
|
|
}
|
|
}
|
|
|
|
async markAsRead(notificationId, element) {
|
|
try {
|
|
const response = await Utils.fetchAPI(`/api/social/notifications/${notificationId}/read`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
element.classList.remove('unread');
|
|
element.querySelector('.notification-mark-read')?.remove();
|
|
this.updateNotificationBadge();
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Markieren als gelesen', 'error');
|
|
}
|
|
}
|
|
|
|
async deleteNotification(notificationId, element) {
|
|
try {
|
|
const response = await Utils.fetchAPI(`/api/social/notifications/${notificationId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.success) {
|
|
element.style.opacity = '0';
|
|
element.style.transform = 'translateX(-100%)';
|
|
setTimeout(() => element.remove(), 300);
|
|
this.updateNotificationBadge();
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Löschen der Benachrichtigung', 'error');
|
|
}
|
|
}
|
|
|
|
async markAllAsRead() {
|
|
try {
|
|
const response = await Utils.fetchAPI('/api/social/notifications/mark-all-read', {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
document.querySelectorAll('.notification-item.unread').forEach(item => {
|
|
item.classList.remove('unread');
|
|
item.querySelector('.notification-mark-read')?.remove();
|
|
});
|
|
|
|
this.updateNotificationBadge(0);
|
|
Utils.showToast('Alle Benachrichtigungen als gelesen markiert', 'success');
|
|
}
|
|
} catch (error) {
|
|
Utils.showToast('Fehler beim Markieren aller Benachrichtigungen', 'error');
|
|
}
|
|
}
|
|
|
|
updateNotificationBadge(count) {
|
|
const badge = document.querySelector('.notification-badge');
|
|
if (badge) {
|
|
if (count === undefined) {
|
|
// Count unread notifications
|
|
count = document.querySelectorAll('.notification-item.unread').length;
|
|
}
|
|
|
|
if (count > 0) {
|
|
badge.textContent = count > 99 ? '99+' : count;
|
|
badge.style.display = 'block';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
pollForNewNotifications() {
|
|
setInterval(async () => {
|
|
try {
|
|
const response = await Utils.fetchAPI('/api/social/notifications?filter=unread&per_page=1');
|
|
if (response.success && response.unread_count !== undefined) {
|
|
this.updateNotificationBadge(response.unread_count);
|
|
}
|
|
} catch (error) {
|
|
// Silent fail for polling
|
|
}
|
|
}, 30000); // Poll every 30 seconds
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SEARCH FUNCTIONALITY
|
|
// =============================================================================
|
|
|
|
class SearchManager {
|
|
constructor() {
|
|
this.searchInput = document.getElementById('global-search');
|
|
this.searchResults = document.getElementById('search-results');
|
|
this.isSearching = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
if (!this.searchInput) return;
|
|
|
|
this.bindEvents();
|
|
this.setupClickOutside();
|
|
}
|
|
|
|
bindEvents() {
|
|
this.searchInput.addEventListener('input',
|
|
Utils.debounce(this.handleSearch.bind(this), 300)
|
|
);
|
|
|
|
this.searchInput.addEventListener('focus', this.showSearchResults.bind(this));
|
|
this.searchInput.addEventListener('keydown', this.handleKeyNavigation.bind(this));
|
|
}
|
|
|
|
setupClickOutside() {
|
|
document.addEventListener('click', (event) => {
|
|
if (!event.target.closest('.search-container')) {
|
|
this.hideSearchResults();
|
|
}
|
|
});
|
|
}
|
|
|
|
async handleSearch(event) {
|
|
const query = event.target.value.trim();
|
|
|
|
if (query.length < 2) {
|
|
this.hideSearchResults();
|
|
return;
|
|
}
|
|
|
|
if (this.isSearching) return;
|
|
this.isSearching = true;
|
|
|
|
try {
|
|
this.showLoadingResults();
|
|
|
|
const [usersResponse, postsResponse] = await Promise.all([
|
|
Utils.fetchAPI(`/api/social/users/search?q=${encodeURIComponent(query)}&limit=5`),
|
|
Utils.fetchAPI(`/api/social/posts?search=${encodeURIComponent(query)}&per_page=5`)
|
|
]);
|
|
|
|
const results = {
|
|
users: usersResponse.success ? usersResponse.users : [],
|
|
posts: postsResponse.success ? postsResponse.posts : []
|
|
};
|
|
|
|
this.renderSearchResults(results, query);
|
|
} catch (error) {
|
|
this.showErrorResults();
|
|
} finally {
|
|
this.isSearching = false;
|
|
}
|
|
}
|
|
|
|
showSearchResults() {
|
|
if (this.searchResults) {
|
|
this.searchResults.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
hideSearchResults() {
|
|
if (this.searchResults) {
|
|
this.searchResults.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
showLoadingResults() {
|
|
if (!this.searchResults) return;
|
|
|
|
this.searchResults.innerHTML = `
|
|
<div class="p-4 text-center">
|
|
<div class="loading-spinner mx-auto mb-2"></div>
|
|
<p class="text-gray-500">Suche läuft...</p>
|
|
</div>
|
|
`;
|
|
this.showSearchResults();
|
|
}
|
|
|
|
showErrorResults() {
|
|
if (!this.searchResults) return;
|
|
|
|
this.searchResults.innerHTML = `
|
|
<div class="p-4 text-center text-red-500">
|
|
<i class="fas fa-exclamation-triangle mb-2"></i>
|
|
<p>Fehler bei der Suche</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
renderSearchResults(results, query) {
|
|
if (!this.searchResults) return;
|
|
|
|
let html = '';
|
|
|
|
if (results.users.length > 0) {
|
|
html += `
|
|
<div class="search-section">
|
|
<h4 class="search-section-title">Benutzer</h4>
|
|
${results.users.map(user => this.createUserResultHTML(user)).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (results.posts.length > 0) {
|
|
html += `
|
|
<div class="search-section">
|
|
<h4 class="search-section-title">Posts</h4>
|
|
${results.posts.map(post => this.createPostResultHTML(post)).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (results.users.length === 0 && results.posts.length === 0) {
|
|
html = `
|
|
<div class="p-4 text-center text-gray-500">
|
|
<i class="fas fa-search mb-2"></i>
|
|
<p>Keine Ergebnisse für "${Utils.escapeHtml(query)}"</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
this.searchResults.innerHTML = html;
|
|
this.showSearchResults();
|
|
}
|
|
|
|
createUserResultHTML(user) {
|
|
return `
|
|
<a href="/profile/${user.username}" class="search-result-item">
|
|
<img src="${user.avatar_url || '/static/img/default-avatar.jpg'}"
|
|
alt="${Utils.escapeHtml(user.display_name)}"
|
|
class="search-result-avatar">
|
|
<div class="search-result-content">
|
|
<h5 class="search-result-name">${Utils.escapeHtml(user.display_name)}</h5>
|
|
<p class="search-result-username">@${Utils.escapeHtml(user.username)}</p>
|
|
</div>
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
createPostResultHTML(post) {
|
|
const truncatedContent = post.content.length > 100
|
|
? post.content.substring(0, 100) + '...'
|
|
: post.content;
|
|
|
|
return `
|
|
<div class="search-result-item">
|
|
<div class="search-result-content">
|
|
<h5 class="search-result-name">${Utils.escapeHtml(post.author.display_name)}</h5>
|
|
<p class="search-result-text">${Utils.escapeHtml(truncatedContent)}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
handleKeyNavigation(event) {
|
|
const results = this.searchResults?.querySelectorAll('.search-result-item');
|
|
if (!results || results.length === 0) return;
|
|
|
|
let current = this.searchResults.querySelector('.search-result-item.highlighted');
|
|
let currentIndex = current ? Array.from(results).indexOf(current) : -1;
|
|
|
|
switch (event.key) {
|
|
case 'ArrowDown':
|
|
event.preventDefault();
|
|
currentIndex = Math.min(currentIndex + 1, results.length - 1);
|
|
break;
|
|
case 'ArrowUp':
|
|
event.preventDefault();
|
|
currentIndex = Math.max(currentIndex - 1, 0);
|
|
break;
|
|
case 'Enter':
|
|
event.preventDefault();
|
|
if (current) current.click();
|
|
return;
|
|
case 'Escape':
|
|
this.hideSearchResults();
|
|
this.searchInput.blur();
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// Update highlighting
|
|
results.forEach(result => result.classList.remove('highlighted'));
|
|
if (currentIndex >= 0) {
|
|
results[currentIndex].classList.add('highlighted');
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// INITIALIZATION
|
|
// =============================================================================
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Initialize components based on current page
|
|
const currentPath = window.location.pathname;
|
|
|
|
if (currentPath === '/social/feed' || currentPath === '/') {
|
|
new SocialFeed();
|
|
}
|
|
|
|
if (currentPath === '/social/notifications') {
|
|
new NotificationCenter();
|
|
}
|
|
|
|
// Initialize global components
|
|
new SearchManager();
|
|
|
|
// Theme toggle functionality
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
if (themeToggle) {
|
|
themeToggle.addEventListener('click', () => {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('theme', newTheme);
|
|
|
|
const icon = themeToggle.querySelector('i');
|
|
icon.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
|
});
|
|
}
|
|
|
|
// Load saved theme
|
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
|
|
// Mobile menu toggle
|
|
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
|
|
if (mobileMenuToggle && mobileMenu) {
|
|
mobileMenuToggle.addEventListener('click', () => {
|
|
mobileMenu.classList.toggle('hidden');
|
|
});
|
|
}
|
|
|
|
// Auto-hide loading states
|
|
setTimeout(() => {
|
|
document.querySelectorAll('.loading-spinner').forEach(spinner => {
|
|
if (spinner.parentElement) {
|
|
spinner.parentElement.style.opacity = '0';
|
|
setTimeout(() => spinner.remove(), 300);
|
|
}
|
|
});
|
|
}, 2000);
|
|
|
|
console.log('🚀 SysTades Social Network initialized successfully!');
|
|
});
|