/** * TXA Comment Widget - TPHIMX Cinematic Platform * Tự động hóa khung bình luận cao cấp. */ class TxaComment { constructor(containerId) { this.container = document.getElementById(containerId); if (!this.container) return; this.movieSlug = this.container.getAttribute('data-movie-slug'); this.movieType = this.container.getAttribute('data-movie-type') || null; this.episodeName = this.container.getAttribute('data-episode-name') || null; this.serverName = this.container.getAttribute('data-server-name') || null; const userAttr = this.container.getAttribute('data-user'); this.user = userAttr ? JSON.parse(userAttr) : null; this.comments = []; this.bannedUsers = []; this.init(); } async init() { this.renderSkeleton(); await this.fetchComments(); this.renderCommentsList(); this.setupListeners(); } renderSkeleton() { const isLogged = !!this.user; this.container.innerHTML = `

Bình luận (0)

${isLogged ? `
0 / 1000
Tiết lộ?
` : `
Vui lòng đăng nhập để tham gia bình luận.
`}
Đang tải bình luận...
`; } async fetchComments() { try { const res = await fetch(`/api/movie/comments?slug=${this.movieSlug}`); if (res.ok) { const data = await res.json(); this.comments = data.comments || []; this.bannedUsers = data.bannedUsers || []; this.userProfiles = data.userProfiles || {}; } } catch (e) { console.error('[TxaComment] Error fetching comments:', e); } } renderCommentsList() { const listContainer = document.getElementById('txac-list'); const countSpan = document.getElementById('txac-count'); if (countSpan) { const totalCount = this.comments.length; countSpan.textContent = window.txaformat ? window.txaformat.twoDigits(totalCount) : (totalCount < 10 ? '0' + totalCount : totalCount); } if (!listContainer) return; if (this.comments.length === 0) { listContainer.innerHTML = `
Chưa có bình luận nào. Hãy là người đầu tiên chia sẻ cảm nghĩ!
`; return; } listContainer.innerHTML = this.comments.map(c => { const charCode = c.username ? c.username.charCodeAt(0) : 65; const avatarColors = [ 'bg-primary text-dark', 'bg-emerald-500 text-white', 'bg-blue-500 text-white', 'bg-pink-500 text-white', 'bg-yellow-500 text-dark', 'bg-purple-500 text-white' ]; const avatarStyle = avatarColors[charCode % avatarColors.length]; // Spoiler Logic const isSpoiler = c.is_spoiler; const commentBody = isSpoiler ? `
Bình luận có chứa tình tiết phim (Click để xem) ${c.content}
` : `

${c.content}

`; // Episode Tag (If commented from watch page) const episodeTag = (c.episode_name && c.server_name) ? `${c.episode_name} - ${c.server_name}` : ''; // Likes/Dislikes Arrays const likes = c.likes || []; const dislikes = c.dislikes || []; const hasLiked = this.user && likes.includes(this.user.username); const hasDisliked = this.user && dislikes.includes(this.user.username); // Actions Check (Delete comment) const canDelete = this.user && (c.username === this.user.username || this.user.role === 'admin'); const deleteButton = canDelete ? `` : ''; // Ban Button (For Admin ONLY) const isBanned = this.bannedUsers.includes(c.username); const showBanBtn = this.user && this.user.role === 'admin' && c.username !== this.user.username; const banButton = showBanBtn ? (isBanned ? `` : `` ) : ''; // Lấy thông tin profile phục vụ cho hover popup const profile = (this.userProfiles && this.userProfiles[c.username]) || { commentsCount: 0, likesCount: 0, ward: '', province: '' }; let locationStr = 'Chưa cập nhật'; if (profile.ward || profile.province) { const parts = []; if (profile.ward) parts.push(`xã ${profile.ward}`); if (profile.province) parts.push(`tỉnh ${profile.province}`); locationStr = parts.join(', '); locationStr = locationStr.charAt(0).toUpperCase() + locationStr.slice(1); } const formattedComments = window.txaformat ? window.txaformat.twoDigits(profile.commentsCount) : (profile.commentsCount < 10 ? '0' + profile.commentsCount : profile.commentsCount); const formattedLikes = window.txaformat ? window.txaformat.twoDigits(profile.likesCount) : (profile.likesCount < 10 ? '0' + profile.likesCount : profile.likesCount); // Check if user has custom avatar URL const hasAvatar = profile && profile.avatar_url; const avatarInner = hasAvatar ? `` : (c.username ? c.username[0] : 'U'); return `
${avatarInner}
@${c.username}
${avatarInner}
@${c.username}
Thành viên TPhimX
Số bình luận: ${formattedComments}
Đã thích: ${formattedLikes} phim
Nơi ở: ${locationStr}
${episodeTag} ${(window.txaformat && window.txaformat.relativeTime) ? window.txaformat.relativeTime(c.created_at) : this.formatRelativeTime(c.created_at)} ${banButton} ${deleteButton}
${commentBody}
`; }).join(''); if (window.initTxaTooltips) { setTimeout(() => window.initTxaTooltips(), 50); } } setupListeners() { const textarea = document.getElementById('txac-textarea'); const counter = document.getElementById('txac-char-counter'); const submitBtn = document.getElementById('txac-submit-btn'); const spoiler = document.getElementById('txac-spoiler'); // Char count update if (textarea && counter) { textarea.addEventListener('input', () => { const len = textarea.value.length; counter.textContent = `${len} / 1000`; }); } // Submit new comment if (submitBtn && textarea) { submitBtn.addEventListener('click', async (e) => { e.preventDefault(); const content = textarea.value.trim(); if (!content) { if (window.txatoast) window.txatoast.warning('Nội dung bình luận không được để trống!', 3000, 'bottom-right'); return; } submitBtn.disabled = true; try { const res = await fetch('/api/movie/comments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ movieSlug: this.movieSlug, content: content, isSpoiler: spoiler ? spoiler.checked : false, episodeName: this.episodeName, serverName: this.serverName, movieType: this.movieType }) }); const data = await res.json(); if (res.ok) { if (window.txatoast) window.txatoast.success(data.message, 3000, 'bottom-right'); textarea.value = ''; if (counter) counter.textContent = '0 / 1000'; if (spoiler) spoiler.checked = false; // Reload comments await this.fetchComments(); this.renderCommentsList(); } else { if (window.txatoast) window.txatoast.error(data.error, 3000, 'bottom-right'); } } catch (err) { if (window.txatoast) window.txatoast.error('Lỗi gửi bình luận!', 3000, 'bottom-right'); } finally { submitBtn.disabled = false; } }); } // Event delegation for Likes, Dislikes, Delete, Ban buttons const listContainer = document.getElementById('txac-list'); if (listContainer) { listContainer.addEventListener('click', async (e) => { // Find reaction button (Like / Dislike) const reactBtn = e.target.closest('.txac-reaction-btn'); if (reactBtn) { e.preventDefault(); const commentId = reactBtn.getAttribute('data-id'); const action = reactBtn.getAttribute('data-action'); await this.handleReaction(commentId, action); return; } // Find delete button const deleteBtn = e.target.closest('.txac-delete-btn'); if (deleteBtn) { e.preventDefault(); const commentId = deleteBtn.getAttribute('data-id'); this.handleDelete(commentId); return; } // Find ban button const banBtn = e.target.closest('.txac-ban-btn'); if (banBtn) { e.preventDefault(); const targetUsername = banBtn.getAttribute('data-username'); const action = banBtn.getAttribute('data-action'); this.handleBan(targetUsername, action); return; } }); } } async handleReaction(commentId, action) { if (!this.user) { if (window.txatoast) window.txatoast.warning('Vui lòng đăng nhập để bình chọn bình luận!', 3000, 'bottom-right'); return; } try { const res = await fetch('/api/movie/comments', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ commentId, action }) }); const data = await res.json(); if (res.ok) { // Cập nhật mảng trực tiếp cho comment hiện tại và re-render để hiển thị thay đổi lập tức const comment = this.comments.find(c => c.id === commentId); if (comment) { comment.likes = data.likes || []; comment.dislikes = data.dislikes || []; this.renderCommentsList(); } } else { if (window.txatoast) window.txatoast.error(data.error, 3000, 'bottom-right'); } } catch (err) { console.error('[TxaComment] Lỗi tương tác:', err); } } handleDelete(commentId) { if (!window.txamodal) { if (confirm('Bạn có chắc chắn muốn xóa bình luận này không?')) { this.executeDelete(commentId); } return; } window.txamodal.show({ title: 'Xóa bình luận', message: 'Bạn có chắc chắn muốn xóa vĩnh viễn bình luận này? Hành động này không thể hoàn tác.', type: 'danger', confirmText: 'Xóa ngay', onConfirm: async () => { await this.executeDelete(commentId); return true; } }); } async executeDelete(commentId) { try { const res = await fetch('/api/movie/comments', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ commentId }) }); const data = await res.json(); if (res.ok) { if (window.txatoast) window.txatoast.success(data.message, 3000, 'bottom-right'); this.comments = this.comments.filter(c => c.id !== commentId); this.renderCommentsList(); } else { if (window.txatoast) window.txatoast.error(data.error, 3000, 'bottom-right'); } } catch (err) { if (window.txatoast) window.txatoast.error('Lỗi khi xóa bình luận!', 3000, 'bottom-right'); } } handleBan(targetUsername, action) { // Chặn 1-click siêu tốc không cần xác nhận this.executeBan(targetUsername, action); } async executeBan(targetUsername, action) { try { const res = await fetch('/api/movie/comments', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, targetUsername }) }); const data = await res.json(); if (res.ok) { if (window.txatoast) window.txatoast.success(data.message, 3000, 'bottom-right'); this.bannedUsers = data.bannedUsers || []; this.renderCommentsList(); } else { if (window.txatoast) window.txatoast.error(data.error, 3000, 'bottom-right'); } } catch (err) { if (window.txatoast) window.txatoast.error('Lỗi khi thực thi yêu cầu chặn!', 3000, 'bottom-right'); } } formatRelativeTime(isoString) { if (!isoString) return ''; const date = new Date(isoString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); if (isNaN(date.getTime()) || diffMs < 0) return 'Vừa xong'; const diffSec = Math.floor(diffMs / 1000); if (diffSec < 60) return 'Vừa xong'; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return `${diffMin} phút trước`; const diffHour = Math.floor(diffMin / 60); if (diffHour < 24) return `${diffHour} giờ trước`; // Vượt quá 1 ngày (24 giờ): Chuyển sang định dạng ngày cụ thể "ngày/tháng/năm" chuẩn Việt Nam const day = date.getDate(); const month = date.getMonth() + 1; const year = date.getFullYear(); return `${day}/${month}/${year}`; } } // Auto instantiate on load function initTxaCommentWidget() { if (document.getElementById('txa-comment-container')) { window.txacomment = new TxaComment('txa-comment-container'); } } document.addEventListener('DOMContentLoaded', initTxaCommentWidget); document.addEventListener('astro:page-load', initTxaCommentWidget);