mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 14:11:09 -05:00
285 lines
9.4 KiB
JavaScript
285 lines
9.4 KiB
JavaScript
// Inline Comment Panel Component
|
|
|
|
class InlineCommentPanel {
|
|
constructor(options = {}) {
|
|
this.container = null;
|
|
this.thread = null;
|
|
this.canResolve = options.canResolve || false;
|
|
this.onReply = options.onReply || (() => {});
|
|
this.onResolve = options.onResolve || (() => {});
|
|
this.currentUser = options.currentUser;
|
|
}
|
|
|
|
initialize(containerId) {
|
|
this.container = document.getElementById(containerId);
|
|
if (!this.container) {
|
|
throw new Error(`Container element with id "${containerId}" not found`);
|
|
}
|
|
}
|
|
|
|
setThread(thread) {
|
|
this.thread = thread;
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
if (!this.container || !this.thread) return;
|
|
|
|
this.container.innerHTML = `
|
|
<div class="comment-panel">
|
|
<div class="comment-header">
|
|
<div class="thread-info">
|
|
<span class="anchor-info">${this.formatAnchor(this.thread.anchor)}</span>
|
|
${this.thread.is_resolved ?
|
|
`<span class="resolution-badge">
|
|
<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
|
</svg>
|
|
Resolved
|
|
</span>`
|
|
: ''
|
|
}
|
|
</div>
|
|
${this.canResolve && !this.thread.is_resolved ?
|
|
`<button class="resolve-button" onclick="this.resolveThread()">
|
|
Resolve Thread
|
|
</button>`
|
|
: ''
|
|
}
|
|
</div>
|
|
<div class="comments-container">
|
|
${this.renderComments(this.thread.comments)}
|
|
</div>
|
|
${!this.thread.is_resolved ?
|
|
`<div class="reply-form">
|
|
<textarea
|
|
class="reply-input"
|
|
placeholder="Write a reply..."
|
|
rows="2"
|
|
></textarea>
|
|
<div class="form-actions">
|
|
<button class="reply-button" onclick="this.submitReply()">
|
|
Reply
|
|
</button>
|
|
</div>
|
|
</div>`
|
|
: ''
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
this.attachEventListeners();
|
|
}
|
|
|
|
renderComments(comments) {
|
|
return comments.map(comment => `
|
|
<div class="comment ${comment.parent_comment ? 'reply' : ''}" data-comment-id="${comment.id}">
|
|
<div class="comment-author">
|
|
<img src="${comment.author.avatar_url || '/static/images/default-avatar.png'}"
|
|
alt="${comment.author.username}"
|
|
class="author-avatar"
|
|
/>
|
|
<span class="author-name">${comment.author.username}</span>
|
|
<span class="comment-date">${this.formatDate(comment.created_at)}</span>
|
|
</div>
|
|
<div class="comment-content">
|
|
${this.formatCommentContent(comment.content)}
|
|
</div>
|
|
${this.renderCommentActions(comment)}
|
|
${comment.replies ?
|
|
`<div class="replies">
|
|
${this.renderComments(comment.replies)}
|
|
</div>`
|
|
: ''
|
|
}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
renderCommentActions(comment) {
|
|
if (this.thread.is_resolved) return '';
|
|
|
|
return `
|
|
<div class="comment-actions">
|
|
<button class="action-button" onclick="this.showReplyForm('${comment.id}')">
|
|
Reply
|
|
</button>
|
|
${comment.author.id === this.currentUser.id ?
|
|
`<button class="action-button" onclick="this.editComment('${comment.id}')">
|
|
Edit
|
|
</button>`
|
|
: ''
|
|
}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
formatCommentContent(content) {
|
|
// Replace @mentions with styled spans
|
|
content = content.replace(/@(\w+)/g, '<span class="mention">@$1</span>');
|
|
|
|
// Convert URLs to links
|
|
content = content.replace(
|
|
/(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/g,
|
|
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
|
|
);
|
|
|
|
return content;
|
|
}
|
|
|
|
formatAnchor(anchor) {
|
|
const start = anchor.line_start;
|
|
const end = anchor.line_end;
|
|
const file = anchor.file_path.split('/').pop();
|
|
|
|
return end > start ?
|
|
`${file}:${start}-${end}` :
|
|
`${file}:${start}`;
|
|
}
|
|
|
|
formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
attachEventListeners() {
|
|
const replyInput = this.container.querySelector('.reply-input');
|
|
if (replyInput) {
|
|
replyInput.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
this.submitReply();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async submitReply() {
|
|
const input = this.container.querySelector('.reply-input');
|
|
const content = input.value.trim();
|
|
|
|
if (!content) return;
|
|
|
|
try {
|
|
await this.onReply(content);
|
|
input.value = '';
|
|
} catch (error) {
|
|
console.error('Failed to submit reply:', error);
|
|
// Show error message to user
|
|
}
|
|
}
|
|
|
|
async resolveThread() {
|
|
try {
|
|
await this.onResolve();
|
|
this.render();
|
|
} catch (error) {
|
|
console.error('Failed to resolve thread:', error);
|
|
// Show error message to user
|
|
}
|
|
}
|
|
|
|
showReplyForm(commentId) {
|
|
const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`);
|
|
if (!comment) return;
|
|
|
|
const replyForm = document.createElement('div');
|
|
replyForm.className = 'reply-form nested';
|
|
replyForm.innerHTML = `
|
|
<textarea
|
|
class="reply-input"
|
|
placeholder="Write a reply..."
|
|
rows="2"
|
|
></textarea>
|
|
<div class="form-actions">
|
|
<button class="cancel-button" onclick="this.hideReplyForm('${commentId}')">
|
|
Cancel
|
|
</button>
|
|
<button class="reply-button" onclick="this.submitNestedReply('${commentId}')">
|
|
Reply
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
comment.appendChild(replyForm);
|
|
replyForm.querySelector('.reply-input').focus();
|
|
}
|
|
|
|
hideReplyForm(commentId) {
|
|
const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`);
|
|
if (!comment) return;
|
|
|
|
const replyForm = comment.querySelector('.reply-form');
|
|
if (replyForm) {
|
|
replyForm.remove();
|
|
}
|
|
}
|
|
|
|
async submitNestedReply(parentId) {
|
|
const comment = this.container.querySelector(`[data-comment-id="${parentId}"]`);
|
|
if (!comment) return;
|
|
|
|
const input = comment.querySelector('.reply-input');
|
|
const content = input.value.trim();
|
|
|
|
if (!content) return;
|
|
|
|
try {
|
|
await this.onReply(content, parentId);
|
|
this.hideReplyForm(parentId);
|
|
} catch (error) {
|
|
console.error('Failed to submit reply:', error);
|
|
// Show error message to user
|
|
}
|
|
}
|
|
|
|
editComment(commentId) {
|
|
const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`);
|
|
if (!comment) return;
|
|
|
|
const contentDiv = comment.querySelector('.comment-content');
|
|
const content = contentDiv.textContent;
|
|
|
|
contentDiv.innerHTML = `
|
|
<textarea class="edit-input" rows="2">${content}</textarea>
|
|
<div class="form-actions">
|
|
<button class="cancel-button" onclick="this.cancelEdit('${commentId}')">
|
|
Cancel
|
|
</button>
|
|
<button class="save-button" onclick="this.saveEdit('${commentId}')">
|
|
Save
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
cancelEdit(commentId) {
|
|
// Refresh the entire thread to restore original content
|
|
this.render();
|
|
}
|
|
|
|
async saveEdit(commentId) {
|
|
const comment = this.container.querySelector(`[data-comment-id="${commentId}"]`);
|
|
if (!comment) return;
|
|
|
|
const input = comment.querySelector('.edit-input');
|
|
const content = input.value.trim();
|
|
|
|
if (!content) return;
|
|
|
|
try {
|
|
// Emit edit event
|
|
const event = new CustomEvent('comment-edited', {
|
|
detail: { commentId, content }
|
|
});
|
|
this.container.dispatchEvent(event);
|
|
|
|
// Refresh the thread
|
|
this.render();
|
|
} catch (error) {
|
|
console.error('Failed to edit comment:', error);
|
|
// Show error message to user
|
|
}
|
|
}
|
|
}
|
|
|
|
export default InlineCommentPanel; |