fix: Fix YAML syntax errors and validator prompt formatting
- Fix YAML files with unquoted strings containing quotes - Export singleton instances in ai/__init__.py - Fix validator scoring prompt to use replace() instead of format() to avoid conflicts with JSON curly braces Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,449 +1,156 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Posts - Social Media Automation</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #1a1a2e; color: #eee; }
|
||||
.card { background-color: #16213e; border-radius: 12px; }
|
||||
.accent { color: #d4a574; }
|
||||
.btn-primary { background-color: #d4a574; color: #1a1a2e; }
|
||||
.btn-primary:hover { background-color: #c49564; }
|
||||
.btn-secondary { background-color: #374151; }
|
||||
.btn-secondary:hover { background-color: #4b5563; }
|
||||
.btn-danger { background-color: #dc2626; }
|
||||
.btn-danger:hover { background-color: #b91c1c; }
|
||||
.status-published { background-color: #065f46; color: #6ee7b7; }
|
||||
.status-scheduled { background-color: #1e40af; color: #93c5fd; }
|
||||
.status-pending { background-color: #92400e; color: #fcd34d; }
|
||||
.status-draft { background-color: #374151; color: #9ca3af; }
|
||||
.status-failed { background-color: #991b1b; color: #fca5a5; }
|
||||
.platform-x { background-color: #1d1d1d; }
|
||||
.platform-threads { background-color: #1a1a1a; }
|
||||
.platform-instagram { background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888); }
|
||||
.platform-facebook { background-color: #1877f2; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Posts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="animate-fade-in">
|
||||
<!-- Header -->
|
||||
<header class="bg-gray-900 border-b border-gray-800 px-6 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">
|
||||
<span class="accent">Consultoría AS</span> - Social Media
|
||||
</h1>
|
||||
<nav class="flex gap-4">
|
||||
<a href="/dashboard" class="px-4 py-2 rounded hover:bg-gray-800">Home</a>
|
||||
<a href="/dashboard/compose" class="px-4 py-2 rounded hover:bg-gray-800">+ Crear</a>
|
||||
<a href="/dashboard/posts" class="px-4 py-2 rounded bg-gray-800 accent">Posts</a>
|
||||
<a href="/dashboard/calendar" class="px-4 py-2 rounded hover:bg-gray-800">Calendario</a>
|
||||
<a href="/dashboard/interactions" class="px-4 py-2 rounded hover:bg-gray-800">Interacciones</a>
|
||||
<a href="/dashboard/settings" class="px-4 py-2 rounded hover:bg-gray-800">Config</a>
|
||||
<a href="/logout" class="px-4 py-2 rounded hover:bg-gray-800 text-red-400">Salir</a>
|
||||
</nav>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Posts</h1>
|
||||
<p class="text-gray-400 mt-1">Gestiona todas tus publicaciones</p>
|
||||
</div>
|
||||
</header>
|
||||
<a href="/compose" class="btn-primary px-6 py-3 rounded-xl font-medium flex items-center gap-2 transition-all">
|
||||
<span>✍️</span>
|
||||
<span>Crear Post</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto px-6 py-8">
|
||||
<!-- Filters -->
|
||||
<div class="card p-4 mb-6">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<div>
|
||||
<label class="text-sm text-gray-400 block mb-1">Estado</label>
|
||||
<select id="filter-status" onchange="filterPosts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||
<option value="">Todos</option>
|
||||
<option value="published">Publicados</option>
|
||||
<option value="scheduled">Programados</option>
|
||||
<option value="pending_approval">Pendientes</option>
|
||||
<option value="draft">Borradores</option>
|
||||
<option value="failed">Fallidos</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-gray-400 block mb-1">Plataforma</label>
|
||||
<select id="filter-platform" onchange="filterPosts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||
<option value="">Todas</option>
|
||||
<option value="x">X (Twitter)</option>
|
||||
<option value="threads">Threads</option>
|
||||
<option value="instagram">Instagram</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-gray-400 block mb-1">Tipo</label>
|
||||
<select id="filter-type" onchange="filterPosts()" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||
<option value="">Todos</option>
|
||||
<option value="tip">Tips</option>
|
||||
<option value="product">Productos</option>
|
||||
<option value="service">Servicios</option>
|
||||
<option value="engagement">Engagement</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<div>
|
||||
<a href="/dashboard/compose" class="btn-primary px-4 py-2 rounded inline-block">
|
||||
+ Nuevo Post
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-5 gap-4 mb-6">
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-green-400" id="stat-published">0</div>
|
||||
<div class="text-gray-400 text-sm">Publicados</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-blue-400" id="stat-scheduled">0</div>
|
||||
<div class="text-gray-400 text-sm">Programados</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-yellow-400" id="stat-pending">0</div>
|
||||
<div class="text-gray-400 text-sm">Pendientes</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-gray-400" id="stat-draft">0</div>
|
||||
<div class="text-gray-400 text-sm">Borradores</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-red-400" id="stat-failed">0</div>
|
||||
<div class="text-gray-400 text-sm">Fallidos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts List -->
|
||||
<div class="card">
|
||||
<div class="p-4 border-b border-gray-700 flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">Posts</h2>
|
||||
<span class="text-gray-400 text-sm" id="posts-count">0 posts</span>
|
||||
</div>
|
||||
<div id="posts-list" class="divide-y divide-gray-700">
|
||||
<!-- Posts loaded dynamically -->
|
||||
<div class="p-8 text-center text-gray-500">
|
||||
Cargando posts...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex justify-center gap-2 mt-6" id="pagination">
|
||||
<!-- Pagination buttons -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div id="edit-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="card p-6 max-w-2xl w-full mx-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Editar Post</h3>
|
||||
<button onclick="closeEditModal()" class="text-gray-400 hover:text-white">✕</button>
|
||||
</div>
|
||||
<form id="edit-form">
|
||||
<input type="hidden" id="edit-post-id">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-400 mb-1">Contenido</label>
|
||||
<textarea id="edit-content" rows="4" class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2"></textarea>
|
||||
<div class="text-right text-sm text-gray-500 mt-1">
|
||||
<span id="edit-char-count">0</span> caracteres
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-400 mb-1">Programar para</label>
|
||||
<input type="datetime-local" id="edit-scheduled" class="bg-gray-800 border border-gray-700 rounded px-3 py-2">
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" onclick="closeEditModal()" class="btn-secondary px-4 py-2 rounded">Cancelar</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Filters -->
|
||||
<div class="card rounded-2xl p-4 mb-6">
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<select id="filter-status" onchange="filterPosts()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="published">Publicados</option>
|
||||
<option value="scheduled">Programados</option>
|
||||
<option value="pending_approval">Pendientes</option>
|
||||
<option value="draft">Borradores</option>
|
||||
<option value="failed">Fallidos</option>
|
||||
</select>
|
||||
<select id="filter-platform" onchange="filterPosts()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white focus:border-primary focus:outline-none">
|
||||
<option value="">Todas las plataformas</option>
|
||||
<option value="x">X (Twitter)</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
<option value="instagram">Instagram</option>
|
||||
<option value="threads">Threads</option>
|
||||
</select>
|
||||
<input type="text" id="filter-search" placeholder="Buscar..." onkeyup="filterPosts()" class="bg-dark-800 border border-dark-600 rounded-xl px-4 py-2 text-white placeholder-gray-500 focus:border-primary focus:outline-none flex-1 min-w-48">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allPosts = [];
|
||||
let currentPage = 1;
|
||||
const postsPerPage = 20;
|
||||
|
||||
// Load posts on page load
|
||||
window.addEventListener('load', loadPosts);
|
||||
|
||||
async function loadPosts() {
|
||||
try {
|
||||
const response = await fetch('/api/posts/');
|
||||
allPosts = await response.json();
|
||||
updateStats();
|
||||
filterPosts();
|
||||
} catch (error) {
|
||||
console.error('Error loading posts:', error);
|
||||
document.getElementById('posts-list').innerHTML =
|
||||
'<div class="p-8 text-center text-red-400">Error cargando posts</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const stats = {
|
||||
published: 0,
|
||||
scheduled: 0,
|
||||
pending_approval: 0,
|
||||
draft: 0,
|
||||
failed: 0
|
||||
};
|
||||
|
||||
allPosts.forEach(post => {
|
||||
if (stats[post.status] !== undefined) {
|
||||
stats[post.status]++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('stat-published').textContent = stats.published;
|
||||
document.getElementById('stat-scheduled').textContent = stats.scheduled;
|
||||
document.getElementById('stat-pending').textContent = stats.pending_approval;
|
||||
document.getElementById('stat-draft').textContent = stats.draft;
|
||||
document.getElementById('stat-failed').textContent = stats.failed;
|
||||
}
|
||||
|
||||
function filterPosts() {
|
||||
const status = document.getElementById('filter-status').value;
|
||||
const platform = document.getElementById('filter-platform').value;
|
||||
const type = document.getElementById('filter-type').value;
|
||||
|
||||
let filtered = allPosts;
|
||||
|
||||
if (status) {
|
||||
filtered = filtered.filter(p => p.status === status);
|
||||
}
|
||||
if (platform) {
|
||||
filtered = filtered.filter(p => p.platforms && p.platforms.includes(platform));
|
||||
}
|
||||
if (type) {
|
||||
filtered = filtered.filter(p => p.content_type === type);
|
||||
}
|
||||
|
||||
renderPosts(filtered);
|
||||
}
|
||||
|
||||
function renderPosts(posts) {
|
||||
const container = document.getElementById('posts-list');
|
||||
document.getElementById('posts-count').textContent = `${posts.length} posts`;
|
||||
|
||||
if (posts.length === 0) {
|
||||
container.innerHTML = '<div class="p-8 text-center text-gray-500">No hay posts</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const start = (currentPage - 1) * postsPerPage;
|
||||
const pagePosts = posts.slice(start, start + postsPerPage);
|
||||
|
||||
container.innerHTML = pagePosts.map(post => `
|
||||
<div class="p-4 hover:bg-gray-800 transition-colors" data-post-id="${post.id}">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="status-${post.status} px-2 py-1 rounded text-xs font-medium">
|
||||
${getStatusLabel(post.status)}
|
||||
</span>
|
||||
${post.platforms ? post.platforms.map(p => `
|
||||
<span class="platform-${p} px-2 py-1 rounded text-xs">${p}</span>
|
||||
`).join('') : ''}
|
||||
<span class="text-gray-500 text-xs">${post.content_type || 'general'}</span>
|
||||
</div>
|
||||
<p class="text-gray-300 mb-2">${escapeHtml(post.content || '').substring(0, 200)}${post.content && post.content.length > 200 ? '...' : ''}</p>
|
||||
<div class="text-gray-500 text-xs">
|
||||
${post.scheduled_at ? `Programado: ${formatDate(post.scheduled_at)}` : ''}
|
||||
${post.published_at ? `Publicado: ${formatDate(post.published_at)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
${post.status === 'pending_approval' ? `
|
||||
<button onclick="approvePost(${post.id})" class="btn-primary px-3 py-1 rounded text-sm">Aprobar</button>
|
||||
<button onclick="rejectPost(${post.id})" class="btn-danger px-3 py-1 rounded text-sm">Rechazar</button>
|
||||
` : ''}
|
||||
${post.status === 'scheduled' ? `
|
||||
<button onclick="publishNow(${post.id})" class="btn-primary px-3 py-1 rounded text-sm">Publicar Ya</button>
|
||||
<button onclick="cancelPost(${post.id})" class="btn-secondary px-3 py-1 rounded text-sm">Cancelar</button>
|
||||
` : ''}
|
||||
${post.status === 'draft' ? `
|
||||
<button onclick="schedulePost(${post.id})" class="btn-primary px-3 py-1 rounded text-sm">Programar</button>
|
||||
` : ''}
|
||||
<button onclick="editPost(${post.id})" class="btn-secondary px-3 py-1 rounded text-sm">Editar</button>
|
||||
<button onclick="deletePost(${post.id})" class="text-red-400 hover:text-red-300 text-sm">Eliminar</button>
|
||||
</div>
|
||||
<!-- Posts List -->
|
||||
<div id="posts-container" class="space-y-4">
|
||||
{% for post in posts %}
|
||||
<div class="card rounded-2xl p-6 hover:border-primary/30 transition-all post-item"
|
||||
data-status="{{ post.status }}"
|
||||
data-platforms="{{ post.platforms|join(',') if post.platforms else '' }}"
|
||||
data-content="{{ post.content|lower }}">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<p class="text-white mb-3">{{ post.content[:200] }}{% if post.content|length > 200 %}...{% endif %}</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{% for platform in post.platforms %}
|
||||
<span class="text-xs px-2 py-1 rounded-full
|
||||
{% if platform == 'x' %}bg-gray-700{% 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 == 'threads' %}bg-gray-600{% endif %}
|
||||
">{{ platform }}</span>
|
||||
{% endfor %}
|
||||
<span class="text-xs px-2 py-1 rounded-full
|
||||
{% if post.status == 'published' %}bg-green-500/20 text-green-400{% endif %}
|
||||
{% if post.status == 'scheduled' %}bg-blue-500/20 text-blue-400{% endif %}
|
||||
{% if post.status == 'pending_approval' %}bg-yellow-500/20 text-yellow-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 %}
|
||||
">{{ post.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<span class="text-xs text-gray-500">{{ post.created_at[:16] if post.created_at else '-' }}</span>
|
||||
<div class="flex gap-2">
|
||||
{% 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">
|
||||
Publicar
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="deletePost({{ post.id }})" class="text-xs bg-red-500/20 text-red-400 px-3 py-1 rounded-lg hover:bg-red-500/30 transition-colors">
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-16 text-gray-500">
|
||||
<span class="text-6xl mb-4 block">📭</span>
|
||||
<p class="text-xl">No hay posts</p>
|
||||
<p class="mt-2">Crea tu primera publicación</p>
|
||||
<a href="/compose" class="btn-primary px-6 py-3 rounded-xl font-medium inline-flex items-center gap-2 mt-4">
|
||||
<span>✍️</span>
|
||||
<span>Crear Post</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
// Render pagination
|
||||
renderPagination(posts.length);
|
||||
}
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function filterPosts() {
|
||||
const status = document.getElementById('filter-status').value;
|
||||
const platform = document.getElementById('filter-platform').value;
|
||||
const search = document.getElementById('filter-search').value.toLowerCase();
|
||||
|
||||
function renderPagination(totalPosts) {
|
||||
const totalPages = Math.ceil(totalPosts / postsPerPage);
|
||||
const container = document.getElementById('pagination');
|
||||
document.querySelectorAll('.post-item').forEach(post => {
|
||||
const postStatus = post.dataset.status;
|
||||
const postPlatforms = post.dataset.platforms;
|
||||
const postContent = post.dataset.content;
|
||||
|
||||
if (totalPages <= 1) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const matchesStatus = !status || postStatus === status;
|
||||
const matchesPlatform = !platform || postPlatforms.includes(platform);
|
||||
const matchesSearch = !search || postContent.includes(search);
|
||||
|
||||
let html = '';
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `<button onclick="goToPage(${i})" class="${i === currentPage ? 'btn-primary' : 'btn-secondary'} px-3 py-1 rounded">${i}</button>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
filterPosts();
|
||||
}
|
||||
|
||||
function getStatusLabel(status) {
|
||||
const labels = {
|
||||
'published': 'Publicado',
|
||||
'scheduled': 'Programado',
|
||||
'pending_approval': 'Pendiente',
|
||||
'draft': 'Borrador',
|
||||
'failed': 'Fallido'
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('es-MX', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Actions
|
||||
async function approvePost(id) {
|
||||
if (!confirm('¿Aprobar este post para publicación?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/posts/${id}/approve`, { method: 'POST' });
|
||||
loadPosts();
|
||||
} catch (error) {
|
||||
alert('Error al aprobar: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectPost(id) {
|
||||
if (!confirm('¿Rechazar este post?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/posts/${id}/reject`, { method: 'POST' });
|
||||
loadPosts();
|
||||
} catch (error) {
|
||||
alert('Error al rechazar: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function publishNow(id) {
|
||||
if (!confirm('¿Publicar inmediatamente?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/calendar/posts/${id}/publish-now`, { method: 'POST' });
|
||||
alert('Post enviado a publicación');
|
||||
loadPosts();
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelPost(id) {
|
||||
if (!confirm('¿Cancelar programación?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/calendar/posts/${id}/cancel`, { method: 'POST' });
|
||||
loadPosts();
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function schedulePost(id) {
|
||||
try {
|
||||
const response = await fetch(`/api/calendar/posts/${id}/schedule?auto=true`, { method: 'POST' });
|
||||
const data = await response.json();
|
||||
alert(`Programado para: ${formatDate(data.scheduled_at)}`);
|
||||
loadPosts();
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function editPost(id) {
|
||||
const post = allPosts.find(p => p.id === id);
|
||||
if (!post) return;
|
||||
|
||||
document.getElementById('edit-post-id').value = id;
|
||||
document.getElementById('edit-content').value = post.content || '';
|
||||
document.getElementById('edit-char-count').textContent = (post.content || '').length;
|
||||
|
||||
if (post.scheduled_at) {
|
||||
const date = new Date(post.scheduled_at);
|
||||
document.getElementById('edit-scheduled').value = date.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
document.getElementById('edit-modal').classList.remove('hidden');
|
||||
document.getElementById('edit-modal').classList.add('flex');
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-modal').classList.add('hidden');
|
||||
document.getElementById('edit-modal').classList.remove('flex');
|
||||
}
|
||||
|
||||
document.getElementById('edit-content').addEventListener('input', function() {
|
||||
document.getElementById('edit-char-count').textContent = this.value.length;
|
||||
post.style.display = matchesStatus && matchesPlatform && matchesSearch ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('edit-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
async function publishPost(id) {
|
||||
if (!confirm('¿Publicar este post ahora?')) return;
|
||||
|
||||
const id = document.getElementById('edit-post-id').value;
|
||||
const content = document.getElementById('edit-content').value;
|
||||
const scheduled = document.getElementById('edit-scheduled').value;
|
||||
showModal('Publicando...', true);
|
||||
try {
|
||||
const response = await fetch(`/api/posts/${id}/publish`, { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
||||
try {
|
||||
await fetch(`/api/posts/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
scheduled_at: scheduled || null
|
||||
})
|
||||
});
|
||||
|
||||
closeEditModal();
|
||||
loadPosts();
|
||||
} catch (error) {
|
||||
alert('Error al guardar: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
async function deletePost(id) {
|
||||
if (!confirm('¿Eliminar este post permanentemente?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/posts/${id}`, { method: 'DELETE' });
|
||||
loadPosts();
|
||||
} catch (error) {
|
||||
alert('Error al eliminar: ' + error.message);
|
||||
if (data.success) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">🎉</span><p>¡Publicado!</p></div>');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>${data.error || 'Error'}</p></div>`);
|
||||
}
|
||||
} catch (error) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
async function deletePost(id) {
|
||||
if (!confirm('¿Eliminar este post?')) return;
|
||||
|
||||
showModal('Eliminando...', true);
|
||||
try {
|
||||
const response = await fetch(`/api/posts/${id}`, { method: 'DELETE' });
|
||||
|
||||
if (response.ok) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">🗑️</span><p>Eliminado</p></div>');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error eliminando</p></div>');
|
||||
}
|
||||
} catch (error) {
|
||||
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user