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:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user