Files
website/static/js/social.js

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!');
});