Add comment and reply functionality with preview and notification templates

This commit is contained in:
pacnpal
2025-02-07 13:13:49 -05:00
parent 2c4d2daf34
commit 0e0ed01cee
30 changed files with 5153 additions and 383 deletions

View File

@@ -0,0 +1,314 @@
// Version Comparison Component
class VersionComparison {
constructor(options = {}) {
this.container = null;
this.versions = new Map();
this.selectedVersions = new Set();
this.maxSelections = options.maxSelections || 3;
this.onCompare = options.onCompare || (() => {});
this.onRollback = options.onRollback || (() => {});
this.timeline = null;
}
initialize(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
throw new Error(`Container element with id "${containerId}" not found`);
}
this._initializeTimeline();
}
setVersions(versions) {
this.versions = new Map(versions.map(v => [v.name, v]));
this._updateTimeline();
this.render();
}
_initializeTimeline() {
this.timeline = document.createElement('div');
this.timeline.className = 'version-timeline';
this.container.appendChild(this.timeline);
}
_updateTimeline() {
const sortedVersions = Array.from(this.versions.values())
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
this.timeline.innerHTML = `
<div class="timeline-track">
${this._renderTimelineDots(sortedVersions)}
</div>
<div class="timeline-labels">
${this._renderTimelineLabels(sortedVersions)}
</div>
`;
}
_renderTimelineDots(versions) {
return versions.map(version => `
<div class="timeline-point ${this.selectedVersions.has(version.name) ? 'selected' : ''}"
data-version="${version.name}"
style="--impact-score: ${version.comparison_metadata.impact_score || 0}"
onclick="this._toggleVersionSelection('${version.name}')">
<div class="point-indicator"></div>
${this._renderImpactIndicator(version)}
</div>
`).join('');
}
_renderImpactIndicator(version) {
const impact = version.comparison_metadata.impact_score || 0;
const size = Math.max(8, Math.min(24, impact * 24)); // 8-24px based on impact
return `
<div class="impact-indicator"
style="width: ${size}px; height: ${size}px"
title="Impact Score: ${Math.round(impact * 100)}%">
</div>
`;
}
_renderTimelineLabels(versions) {
return versions.map(version => `
<div class="timeline-label" data-version="${version.name}">
<div class="version-name">${version.name}</div>
<div class="version-date">
${new Date(version.created_at).toLocaleDateString()}
</div>
</div>
`).join('');
}
render() {
if (!this.container) return;
const selectedVersionsArray = Array.from(this.selectedVersions);
this.container.innerHTML = `
<div class="version-comparison-tool">
<div class="comparison-header">
<h3>Version Comparison</h3>
<div class="selected-versions">
${this._renderSelectedVersions(selectedVersionsArray)}
</div>
<div class="comparison-actions">
${this._renderActionButtons(selectedVersionsArray)}
</div>
</div>
${this.timeline.outerHTML}
<div class="comparison-content">
${this._renderComparisonContent(selectedVersionsArray)}
</div>
</div>
`;
this.attachEventListeners();
}
_renderSelectedVersions(selectedVersions) {
return selectedVersions.map((version, index) => `
<div class="selected-version">
<span class="version-label">Version ${index + 1}:</span>
<span class="version-value">${version}</span>
<button class="remove-version" onclick="this._removeVersion('${version}')">
×
</button>
</div>
`).join('');
}
_renderActionButtons(selectedVersions) {
const canCompare = selectedVersions.length >= 2;
const canRollback = selectedVersions.length === 1;
return `
${canCompare ? `
<button class="compare-button" onclick="this._handleCompare()">
Compare Versions
</button>
` : ''}
${canRollback ? `
<button class="rollback-button" onclick="this._handleRollback()">
Rollback to Version
</button>
` : ''}
`;
}
_renderComparisonContent(selectedVersions) {
if (selectedVersions.length < 2) {
return `
<div class="comparison-placeholder">
Select at least two versions to compare
</div>
`;
}
return `
<div class="comparison-results">
<div class="results-loading">
Computing differences...
</div>
</div>
`;
}
_toggleVersionSelection(versionName) {
if (this.selectedVersions.has(versionName)) {
this.selectedVersions.delete(versionName);
} else if (this.selectedVersions.size < this.maxSelections) {
this.selectedVersions.add(versionName);
} else {
// Show max selections warning
this._showWarning(`Maximum ${this.maxSelections} versions can be compared`);
return;
}
this.render();
}
_removeVersion(versionName) {
this.selectedVersions.delete(versionName);
this.render();
}
async _handleCompare() {
const selectedVersions = Array.from(this.selectedVersions);
if (selectedVersions.length < 2) return;
try {
const results = await this.onCompare(selectedVersions);
this._renderComparisonResults(results);
} catch (error) {
console.error('Comparison failed:', error);
this._showError('Failed to compare versions');
}
}
async _handleRollback() {
const selectedVersion = Array.from(this.selectedVersions)[0];
if (!selectedVersion) return;
try {
await this.onRollback(selectedVersion);
// Handle successful rollback
} catch (error) {
console.error('Rollback failed:', error);
this._showError('Failed to rollback version');
}
}
_renderComparisonResults(results) {
const resultsContainer = this.container.querySelector('.comparison-results');
if (!resultsContainer) return;
resultsContainer.innerHTML = `
<div class="results-content">
${results.map(diff => this._renderDiffSection(diff)).join('')}
</div>
`;
}
_renderDiffSection(diff) {
return `
<div class="diff-section">
<div class="diff-header">
<h4>Changes: ${diff.version1}${diff.version2}</h4>
<div class="diff-stats">
<span class="computation-time">
Computed in ${diff.computation_time.toFixed(2)}s
</span>
<span class="impact-score">
Impact Score: ${Math.round(diff.impact_score * 100)}%
</span>
</div>
</div>
<div class="changes-list">
${this._renderChanges(diff.changes)}
</div>
</div>
`;
}
_renderChanges(changes) {
return changes.map(change => `
<div class="change-item">
<div class="change-header">
<span class="change-type">${change.type}</span>
<span class="change-file">${change.file}</span>
</div>
<div class="change-content">
<div class="old-value">
<div class="value-header">Previous</div>
<pre>${this._escapeHtml(change.old_value)}</pre>
</div>
<div class="new-value">
<div class="value-header">New</div>
<pre>${this._escapeHtml(change.new_value)}</pre>
</div>
</div>
</div>
`).join('');
}
_showWarning(message) {
const warning = document.createElement('div');
warning.className = 'comparison-warning';
warning.textContent = message;
this.container.appendChild(warning);
setTimeout(() => warning.remove(), 3000);
}
_showError(message) {
const error = document.createElement('div');
error.className = 'comparison-error';
error.textContent = message;
this.container.appendChild(error);
setTimeout(() => error.remove(), 3000);
}
_escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
attachEventListeners() {
// Timeline scroll handling
const timeline = this.container.querySelector('.version-timeline');
if (timeline) {
let isDown = false;
let startX;
let scrollLeft;
timeline.addEventListener('mousedown', (e) => {
isDown = true;
timeline.classList.add('active');
startX = e.pageX - timeline.offsetLeft;
scrollLeft = timeline.scrollLeft;
});
timeline.addEventListener('mouseleave', () => {
isDown = false;
timeline.classList.remove('active');
});
timeline.addEventListener('mouseup', () => {
isDown = false;
timeline.classList.remove('active');
});
timeline.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - timeline.offsetLeft;
const walk = (x - startX) * 2;
timeline.scrollLeft = scrollLeft - walk;
});
}
}
}
export default VersionComparison;