/** * @fileoverview Comment Item Component for Rantii * @author retoor * @description Single comment display with voting * @keywords comment, item, reply, discussion, vote */ import { BaseComponent } from './base-component.js'; import { formatRelativeTime } from '../utils/date.js'; class CommentItem extends BaseComponent { static get observedAttributes() { return ['comment-id']; } init() { this.commentData = null; this.isEditing = false; this.render(); this.bindEvents(); } setComment(comment) { this.commentData = comment; this.setAttr('comment-id', comment.id); this.render(); } render() { if (!this.commentData) { this.setHtml('
'); return; } const comment = this.commentData; const isOwner = this.getCurrentUser()?.id === comment.user_id; this.addClass('comment-item'); if (this.isEditing) { this.setHtml(`
`); return; } this.setHtml(`
${comment.user_username} +${comment.user_score}
${isOwner ? `
` : ''}
`); } escapeAttr(str) { if (!str) return ''; return str .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } bindEvents() { this.on(this, 'click', this.handleClick); this.on(this, 'vote', this.handleVote); } handleClick(e) { const editBtn = e.target.closest('.edit-btn'); const deleteBtn = e.target.closest('.delete-btn'); const cancelBtn = e.target.closest('.cancel-edit-btn'); const saveBtn = e.target.closest('.save-edit-btn'); const username = e.target.closest('.comment-username'); const avatar = e.target.closest('user-avatar'); if (editBtn) { this.startEditing(); return; } if (deleteBtn) { this.confirmDelete(); return; } if (cancelBtn) { this.cancelEditing(); return; } if (saveBtn) { this.saveEdit(); return; } if (username || avatar) { this.getRouter()?.goToUser(this.commentData.user_username); } } async handleVote(e) { const { vote, itemId } = e.detail; const voteButtons = this.$('vote-buttons'); if (voteButtons) { voteButtons.disable(); } try { const result = await this.getApi()?.voteComment(itemId, vote); if (result?.success && result.comment) { this.commentData.score = result.comment.score; this.commentData.vote_state = result.comment.vote_state; if (voteButtons) { voteButtons.updateVote(result.comment.score, result.comment.vote_state); voteButtons.enable(); } } } catch (error) { if (voteButtons) { voteButtons.enable(); } } } startEditing() { this.isEditing = true; this.render(); const textarea = this.$('.comment-edit-input'); if (textarea) { textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); } } cancelEditing() { this.isEditing = false; this.render(); } async saveEdit() { const textarea = this.$('.comment-edit-input'); if (!textarea) return; const newBody = textarea.value.trim(); if (!newBody || newBody === this.commentData.body) { this.cancelEditing(); return; } try { const result = await this.getApi()?.updateComment(this.commentData.id, newBody); if (result?.success) { this.commentData.body = newBody; this.isEditing = false; this.render(); this.emit('comment-updated', { comment: this.commentData }); } } catch (error) { this.getApp()?.toast?.error('Failed to update comment'); } } confirmDelete() { if (confirm('Delete this comment?')) { this.deleteComment(); } } async deleteComment() { try { const result = await this.getApi()?.deleteComment(this.commentData.id); if (result?.success) { this.emit('comment-deleted', { commentId: this.commentData.id }); this.remove(); } } catch (error) { this.getApp()?.toast?.error('Failed to delete comment'); } } getCommentId() { return this.commentData?.id || this.getAttr('comment-id'); } } customElements.define('comment-item', CommentItem); export { CommentItem };