- 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>
157 lines
7.8 KiB
HTML
157 lines
7.8 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Posts{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="animate-fade-in">
|
|
<!-- Header -->
|
|
<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>
|
|
<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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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>
|
|
<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 %}
|
|
|
|
{% 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();
|
|
|
|
document.querySelectorAll('.post-item').forEach(post => {
|
|
const postStatus = post.dataset.status;
|
|
const postPlatforms = post.dataset.platforms;
|
|
const postContent = post.dataset.content;
|
|
|
|
const matchesStatus = !status || postStatus === status;
|
|
const matchesPlatform = !platform || postPlatforms.includes(platform);
|
|
const matchesSearch = !search || postContent.includes(search);
|
|
|
|
post.style.display = matchesStatus && matchesPlatform && matchesSearch ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
async function publishPost(id) {
|
|
if (!confirm('¿Publicar este post ahora?')) return;
|
|
|
|
showModal('Publicando...', true);
|
|
try {
|
|
const response = await fetch(`/api/posts/${id}/publish`, { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
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>');
|
|
}
|
|
}
|
|
|
|
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 %}
|