/** * @fileoverview Rant Detail Component for Rantii * @author retoor * @description Full rant view with comments * @keywords rant, detail, view, full, comments */ import { BaseComponent } from './base-component.js'; import { formatRelativeTime, formatFullDate } from '../utils/date.js'; import { buildDevrantImageUrl } from '../utils/url.js'; class RantDetail extends BaseComponent { static get observedAttributes() { return ['rant-id']; } init() { this.rantData = null; this.comments = []; this.isLoading = false; this.render(); this.bindEvents(); } async load(rantId) { this.setAttr('rant-id', rantId); this.isLoading = true; this.render(); try { const result = await this.getApi()?.getRant(rantId); if (result?.success) { this.rantData = result.rant; this.comments = result.comments || []; } } catch (error) { this.rantData = null; this.comments = []; } finally { this.isLoading = false; this.render(); } } setRant(rant, comments = []) { this.rantData = rant; this.comments = comments; this.setAttr('rant-id', rant.id); this.render(); } render() { if (this.isLoading) { this.setHtml(`
`); return; } if (!this.rantData) { this.setHtml(`

Rant not found

`); return; } const rant = this.rantData; const hasImage = rant.attached_image && typeof rant.attached_image === 'object'; const imageUrl = hasImage ? buildDevrantImageUrl(rant.attached_image.url) : null; this.addClass('rant-detail'); this.setHtml(`

Rant

${rant.user_username} +${rant.user_score}
${imageUrl ? `
` : ''}
${rant.tags && rant.tags.length > 0 ? `
${rant.tags.map(tag => ` `).join('')}
` : ''}
${this.comments.length} comments
${this.comments.map(comment => ` `).join('')}
`); this.initComments(); } escapeAttr(str) { if (!str) return ''; return str .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } initComments() { const commentItems = this.$$('comment-item'); commentItems.forEach(item => { const commentData = item.dataset.comment; if (commentData) { try { const comment = JSON.parse(commentData.replace(/'/g, "'")); item.setComment(comment); } catch (e) {} } }); } bindEvents() { this.on(this, 'click', this.handleClick); this.on(this, 'vote', this.handleVote); this.on(this, 'comment-posted', this.handleCommentPosted); } handleClick(e) { const backBtn = e.target.closest('.back-btn'); const username = e.target.closest('.author-username'); const avatar = e.target.closest('user-avatar'); const tag = e.target.closest('.tag'); if (backBtn) { e.preventDefault(); window.history.back(); return; } if (username || avatar) { this.getRouter()?.goToUser(this.rantData.user_username); return; } if (tag) { const tagText = tag.dataset.tag || tag.textContent; this.getRouter()?.goToSearch(tagText); } } async handleVote(e) { const { vote, itemId, type } = e.detail; if (type === 'rant') { const result = await this.getApi()?.voteRant(itemId, vote); if (result?.success && result.rant) { this.rantData.score = result.rant.score; this.rantData.vote_state = result.rant.vote_state; const voteButtons = this.$('vote-buttons[type="rant"]'); if (voteButtons) { voteButtons.updateVote(result.rant.score, result.rant.vote_state); } } } } async handleCommentPosted(e) { await this.load(this.rantData.id); this.scrollToComments(); } scrollToComment(commentId) { const commentEl = this.$(`comment-item[comment-id="${commentId}"]`); if (commentEl) { commentEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); commentEl.classList.add('highlight'); setTimeout(() => commentEl.classList.remove('highlight'), 2000); } } scrollToComments() { const commentsSection = this.$('.comments-section'); if (commentsSection) { commentsSection.scrollIntoView({ behavior: 'smooth' }); } } getRantId() { return this.rantData?.id || this.getAttr('rant-id'); } } customElements.define('rant-detail', RantDetail); export { RantDetail };