feat: Add post detail modal with copy functionality

- Click on any post to open a detailed view modal
- Shows platform-specific content with tabs (X, Threads, etc.)
- Copy button for each platform's content
- Shows quality score and breakdown
- Quick actions: Publish, Delete from modal
- Useful for manually copying Threads posts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 22:57:03 +00:00
parent 1239c4af2c
commit 855e765417

View File

@@ -41,10 +41,11 @@
<!-- Posts List --> <!-- Posts List -->
<div id="posts-container" class="space-y-4"> <div id="posts-container" class="space-y-4">
{% for post in posts %} {% for post in posts %}
<div class="card rounded-2xl p-6 hover:border-primary/30 transition-all post-item" <div class="card rounded-2xl p-6 hover:border-primary/30 transition-all post-item cursor-pointer"
data-status="{{ post.status }}" data-status="{{ post.status }}"
data-platforms="{{ post.platforms|join(',') if post.platforms else '' }}" data-platforms="{{ post.platforms|join(',') if post.platforms else '' }}"
data-content="{{ post.content|lower }}"> data-content="{{ post.content|lower }}"
onclick="viewPost({{ post.id }})">
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div class="flex-1"> <div class="flex-1">
<p class="text-white mb-3">{{ post.content[:200] }}{% if post.content|length > 200 %}...{% endif %}</p> <p class="text-white mb-3">{{ post.content[:200] }}{% if post.content|length > 200 %}...{% endif %}</p>
@@ -54,7 +55,7 @@
{% if platform == 'x' %}bg-gray-700{% endif %} {% if platform == 'x' %}bg-gray-700{% endif %}
{% if platform == 'facebook' %}bg-blue-600/30 text-blue-400{% endif %} {% if platform == 'facebook' %}bg-blue-600/30 text-blue-400{% endif %}
{% if platform == 'instagram' %}bg-pink-600/30 text-pink-400{% endif %} {% if platform == 'instagram' %}bg-pink-600/30 text-pink-400{% endif %}
{% if platform == 'threads' %}bg-gray-600{% endif %} {% if platform == 'threads' %}bg-purple-600/30 text-purple-400{% endif %}
">{{ platform }}</span> ">{{ platform }}</span>
{% endfor %} {% endfor %}
<span class="text-xs px-2 py-1 rounded-full <span class="text-xs px-2 py-1 rounded-full
@@ -64,11 +65,17 @@
{% if post.status == 'draft' %}bg-gray-500/20 text-gray-400{% endif %} {% if post.status == 'draft' %}bg-gray-500/20 text-gray-400{% endif %}
{% if post.status == 'failed' %}bg-red-500/20 text-red-400{% endif %} {% if post.status == 'failed' %}bg-red-500/20 text-red-400{% endif %}
">{{ post.status }}</span> ">{{ post.status }}</span>
{% if post.quality_score %}
<span class="text-xs px-2 py-1 rounded-full bg-primary/20 text-primary">Score: {{ post.quality_score }}</span>
{% endif %}
</div> </div>
</div> </div>
<div class="flex flex-col items-end gap-2"> <div class="flex flex-col items-end gap-2" onclick="event.stopPropagation()">
<span class="text-xs text-gray-500">{{ post.created_at[:16] if post.created_at else '-' }}</span> <span class="text-xs text-gray-500">{{ post.created_at[:16] if post.created_at else '-' }}</span>
<div class="flex gap-2"> <div class="flex gap-2">
<button onclick="viewPost({{ post.id }})" class="text-xs bg-primary/20 text-primary px-3 py-1 rounded-lg hover:bg-primary/30 transition-colors">
Ver
</button>
{% if post.status == 'draft' or post.status == 'pending_approval' %} {% if post.status == 'draft' or post.status == 'pending_approval' %}
<button onclick="publishPost({{ post.id }})" class="text-xs bg-green-500/20 text-green-400 px-3 py-1 rounded-lg hover:bg-green-500/30 transition-colors"> <button onclick="publishPost({{ post.id }})" class="text-xs bg-green-500/20 text-green-400 px-3 py-1 rounded-lg hover:bg-green-500/30 transition-colors">
Publicar Publicar
@@ -97,7 +104,177 @@
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
<!-- Post Detail Modal (larger) -->
<div id="post-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden items-center justify-center z-50">
<div class="card rounded-2xl p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto animate-fade-in">
<div id="post-modal-content"></div>
</div>
</div>
<script> <script>
// View post details
async function viewPost(id) {
const modal = document.getElementById('post-modal');
const content = document.getElementById('post-modal-content');
content.innerHTML = `<div class="flex items-center gap-3"><div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div><span>Cargando...</span></div>`;
modal.classList.remove('hidden');
modal.classList.add('flex');
try {
const response = await fetch(`/api/posts/${id}`);
const post = await response.json();
// Build platform tabs
let platformTabs = '';
let platformContents = '';
const platforms = post.platforms || [];
platforms.forEach((platform, index) => {
const isActive = index === 0;
const platformContent = getPlatformContent(post, platform);
platformTabs += `
<button onclick="switchTab('${platform}')"
id="tab-${platform}"
class="tab-btn px-4 py-2 rounded-lg text-sm transition-colors ${isActive ? 'bg-primary text-white' : 'bg-dark-700 text-gray-400 hover:text-white'}">
${getPlatformIcon(platform)} ${platform}
</button>`;
platformContents += `
<div id="content-${platform}" class="tab-content ${isActive ? '' : 'hidden'}">
<div class="bg-dark-800 rounded-xl p-4 mb-4">
<pre class="whitespace-pre-wrap text-gray-200 font-sans text-sm leading-relaxed">${escapeHtml(platformContent)}</pre>
</div>
<button onclick="copyContent('${platform}')" class="btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<span>📋</span> Copiar contenido
</button>
<input type="hidden" id="copy-${platform}" value="${escapeHtml(platformContent)}">
</div>`;
});
// Status badge color
const statusColors = {
'published': 'bg-green-500/20 text-green-400',
'scheduled': 'bg-blue-500/20 text-blue-400',
'pending_approval': 'bg-yellow-500/20 text-yellow-400',
'draft': 'bg-gray-500/20 text-gray-400',
'failed': 'bg-red-500/20 text-red-400'
};
content.innerHTML = `
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold">Post #${post.id}</h2>
<button onclick="closePostModal()" class="text-gray-400 hover:text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="flex flex-wrap gap-2 mb-4">
<span class="text-xs px-2 py-1 rounded-full ${statusColors[post.status] || 'bg-gray-500/20'}">${post.status}</span>
<span class="text-xs px-2 py-1 rounded-full bg-dark-700">${post.content_type}</span>
${post.quality_score ? `<span class="text-xs px-2 py-1 rounded-full bg-primary/20 text-primary">Score: ${post.quality_score}</span>` : ''}
</div>
<div class="flex gap-2 mb-4 flex-wrap">
${platformTabs}
</div>
${platformContents}
<div class="flex gap-2 mt-6 pt-4 border-t border-dark-700">
${post.status === 'draft' || post.status === 'pending_approval' ?
`<button onclick="closePostModal(); publishPost(${post.id})" class="flex-1 bg-green-500/20 text-green-400 px-4 py-2 rounded-lg hover:bg-green-500/30 transition-colors">
Publicar ahora
</button>` : ''}
<button onclick="closePostModal(); deletePost(${post.id})" class="flex-1 bg-red-500/20 text-red-400 px-4 py-2 rounded-lg hover:bg-red-500/30 transition-colors">
Eliminar
</button>
<button onclick="closePostModal()" class="flex-1 btn-secondary px-4 py-2 rounded-lg">
Cerrar
</button>
</div>
`;
} catch (error) {
content.innerHTML = `
<div class="text-center">
<span class="text-4xl mb-4 block">❌</span>
<p>Error cargando el post</p>
<button onclick="closePostModal()" class="mt-4 btn-secondary px-4 py-2 rounded-lg">Cerrar</button>
</div>`;
}
}
function getPlatformContent(post, platform) {
const contentMap = {
'x': post.content_x,
'threads': post.content_threads,
'instagram': post.content_instagram,
'facebook': post.content_facebook
};
return contentMap[platform] || post.content || '';
}
function getPlatformIcon(platform) {
const icons = { 'x': '𝕏', 'threads': '🧵', 'instagram': '📷', 'facebook': '📘' };
return icons[platform] || '📱';
}
function switchTab(platform) {
// Hide all contents and deactivate tabs
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.tab-btn').forEach(el => {
el.classList.remove('bg-primary', 'text-white');
el.classList.add('bg-dark-700', 'text-gray-400');
});
// Show selected content and activate tab
document.getElementById(`content-${platform}`).classList.remove('hidden');
const tab = document.getElementById(`tab-${platform}`);
tab.classList.remove('bg-dark-700', 'text-gray-400');
tab.classList.add('bg-primary', 'text-white');
}
async function copyContent(platform) {
const content = document.getElementById(`copy-${platform}`).value;
try {
await navigator.clipboard.writeText(content);
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>¡Copiado!</p></div>');
setTimeout(closeModal, 1000);
} catch (err) {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = content;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>¡Copiado!</p></div>');
setTimeout(closeModal, 1000);
}
}
function closePostModal() {
const modal = document.getElementById('post-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Close modal on outside click
document.getElementById('post-modal').addEventListener('click', function(e) {
if (e.target === this) closePostModal();
});
function filterPosts() { function filterPosts() {
const status = document.getElementById('filter-status').value; const status = document.getElementById('filter-status').value;
const platform = document.getElementById('filter-platform').value; const platform = document.getElementById('filter-platform').value;