Fix PostCard image rendering and ID display, plus image backfill automation

This commit is contained in:
Carlos (Almeira) 2026-06-21 08:32:13 -05:00
parent 946eae2a25
commit 81ccb397f5
10 changed files with 351 additions and 114 deletions

View File

@ -1,74 +0,0 @@
const { Client, Databases } = require('node-appwrite');
async function checkConfig() {
console.log('🔍 Validando configuración de Almeira Web...\n');
const requiredVars = [
'NEXT_PUBLIC_APPWRITE_ENDPOINT',
'NEXT_PUBLIC_APPWRITE_PROJECT_ID',
'APPWRITE_API_KEY',
'APPWRITE_DATABASE_ID'
];
let missing = false;
requiredVars.forEach(v => {
if (!process.env[v]) {
console.error(`❌ Faltante: Variable de entorno ${v}`);
missing = true;
} else {
console.log(`✅ Presente: ${v}`);
}
});
if (missing) {
console.error('\n🛑 Error: Faltan variables de entorno críticas. Revisa tu archivo .env.local.');
process.exit(1);
}
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
const databases = new Databases(client);
const databaseId = process.env.APPWRITE_DATABASE_ID;
try {
console.log(`\n📡 Conectando a Appwrite (${process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT})...`);
const db = await databases.get(databaseId);
console.log(`✅ Conexión exitosa a la base de datos: "${db.name}" (${databaseId})`);
const collectionsToCheck = [
'posts',
'settings',
'publication_types',
'categories',
'whatsapp_messages',
'entidad'
];
console.log('\n📦 Verificando colecciones críticas...');
for (const colId of collectionsToCheck) {
try {
const col = await databases.getCollection(databaseId, colId);
console.log(`✅ Colección encontrada: ${col.name} (${colId})`);
} catch (err) {
console.error(`❌ Error en colección ${colId}: ${err.message}`);
missing = true;
}
}
} catch (err) {
console.error(`❌ Error de conexión a Appwrite: ${err.message}`);
process.exit(1);
}
if (missing) {
console.error('\n🛑 Algunas colecciones no fueron encontradas. Revisa la base de datos en Appwrite.');
process.exit(1);
}
console.log('\n✨ ¡Configuración validada correctamente para producción!');
}
checkConfig();

View File

@ -1,18 +0,0 @@
const sdk = require('node-appwrite');
const client = new sdk.Client();
const databases = new sdk.Databases(client);
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
const API_KEY = process.env.APPWRITE_API_KEY || 'standard_ee81a4ca6784c0fa6a23abab0a983dbba9cd0c2d8e211a00c759d9801f5d8e27b43a605bfa373e13106da87acfc7673608f0ffb0eb04e10cae3dae93d2404bfda8676f96e3fcfd6bd642f6e12d69367916b796c54e98e2e4c4bc0b7a27aa42ba5048582fcbd45b1365b51c5014cb939c1853923c418048ff0e39c2237065da3c';
client.setEndpoint(ENDPOINT).setProject(PROJECT_ID).setKey(API_KEY);
async function check() {
try {
console.log("Chequeando Tipos de Publicacion...");
const res = await databases.listDocuments('almeira', 'publication_types', []);
console.log(JSON.stringify(res.documents.map(d => ({id: d.$id, c: d.codigo, d: d.descripcion})), null, 2));
} catch(e) { console.error(e.message); }
}
check();

140
import_openclaw.js Normal file
View File

@ -0,0 +1,140 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { Client, Databases, ID } = require('node-appwrite');
require('dotenv').config({ path: '.env.local' });
const mammoth = require('mammoth');
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
const databases = new Databases(client);
const dbId = process.env.APPWRITE_DATABASE_ID;
const collectionId = 'posts';
const defaultTipoPublicacionId = '69c2b91400362d5d52b6'; // NOTICIA
const DIR_PENDIENTES = '/home/carlos/Documentos/Pendientes';
function generateSlug(titulo) {
return titulo.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-').replace(/-+/g, '-').substring(0, 100);
}
function truncateString(str, length) {
if (!str) return '';
return str.length > length ? str.substring(0, length - 3) + '...' : str;
}
function extractDate(filename) {
const regex = /(\d{1,2})\s+DE\s+([A-Z]+)\s+DEL?\s+(\d{4})/i;
const match = filename.match(regex);
if (match) {
const months = { 'enero': 0, 'febrero': 1, 'marzo': 2, 'abril': 3, 'mayo': 4, 'junio': 5, 'julio': 6, 'agosto': 7, 'septiembre': 8, 'octubre': 9, 'noviembre': 10, 'diciembre': 11 };
const monthStr = match[2].toLowerCase();
if (months[monthStr] !== undefined) {
return new Date(parseInt(match[3], 10), months[monthStr], parseInt(match[1], 10), 12, 0, 0).toISOString();
}
}
return new Date().toISOString();
}
function extractDriveUrl(text) {
const match = text.match(/https:\/\/drive\.google\.com\/[^\s\n]+/);
return match ? match[0] : null;
}
async function insertPost(title, contentBlocks, driveUrl, filename) {
if (!title || contentBlocks.length === 0) return;
let pureContent = contentBlocks.filter(b => !b.includes('http')).map(p => `<p>${p}</p>`).join('');
const docData = {
titulo: truncateString(title, 70),
slug: generateSlug(title) + '-' + Math.random().toString(36).substring(2, 7),
contenido: pureContent,
resumen_redes: truncateString(contentBlocks[0], 155),
seo_titulo: truncateString(title, 60),
seo_descripcion: truncateString(contentBlocks[0], 155),
estado: 'borrador',
tipo_publicacion_id: defaultTipoPublicacionId,
fecha_publicacion: extractDate(filename),
...(driveUrl && { url_destino: driveUrl })
};
try {
await databases.createDocument(dbId, collectionId, ID.unique(), docData);
console.log(`✅ Creado: ${docData.titulo}`);
} catch (e) {
console.error(`❌ Error en ${title}:`, e.message);
}
}
async function processFile(filePath, filename) {
const ext = path.extname(filename).toLowerCase();
if (ext === '.docx') {
const result = await mammoth.extractRawText({ path: filePath });
const lines = result.value.split('\n').map(l => l.trim()).filter(l => l.length > 0);
if (lines.length > 0) {
const title = lines[0];
const contentBlocks = lines.slice(1);
const driveUrl = extractDriveUrl(result.value);
await insertPost(title, contentBlocks, driveUrl, filename);
}
} else if (ext === '.pdf') {
const text = execSync(`pdftotext "${filePath}" -`).toString();
const pages = text.split('\x0C').map(p => p.trim()).filter(p => p.length > 0);
for (const page of pages) {
const lines = page.split('\n').map(l => l.trim()).filter(l => l.length > 0);
if (lines.length < 3) continue;
let startIndex = 0;
if (lines[0].toLowerCase().includes('boletín')) {
startIndex = 1; // skip header
}
const title = lines[startIndex];
const contentBlocks = [];
let currentBlock = [];
for (let i = startIndex + 1; i < lines.length; i++) {
currentBlock.push(lines[i]);
if (lines[i].endsWith('.') || lines[i].includes('http')) {
contentBlocks.push(currentBlock.join(' '));
currentBlock = [];
}
}
if (currentBlock.length > 0) contentBlocks.push(currentBlock.join(' '));
const driveUrl = extractDriveUrl(page);
await insertPost(title, contentBlocks, driveUrl, filename);
}
}
}
async function main() {
let files = fs.readdirSync(DIR_PENDIENTES).filter(f => f.endsWith('.pdf') || f.endsWith('.docx'));
files.sort((a, b) => {
const numA = parseInt(a.match(/\d+/)) || 0;
const numB = parseInt(b.match(/\d+/)) || 0;
return numA - numB;
});
// Only March and April: Boletines 22 to 37
files = files.filter(f => {
const num = parseInt(f.match(/\d+/)) || 0;
return num >= 22 && num <= 37;
});
console.log(`Procesando ${files.length} archivos de Marzo y Abril...`);
for (const file of files) {
console.log(`\n📄 Iniciando: ${file}`);
await processFile(path.join(DIR_PENDIENTES, file), file);
}
console.log('\n¡Proceso finalizado!');
}
main();

166
import_pendientes.js Normal file
View File

@ -0,0 +1,166 @@
const fs = require('fs');
const path = require('path');
const { Client, Databases, ID } = require('node-appwrite');
require('dotenv').config({ path: '.env.local' });
const pdfParse = require('pdf-parse');
const mammoth = require('mammoth');
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
const databases = new Databases(client);
const dbId = process.env.APPWRITE_DATABASE_ID;
const collectionId = 'posts';
const defaultTipoPublicacionId = '69c2b91400362d5d52b6'; // ID de NOTICIA obtenido anteriormente
const DIR_PENDIENTES = '/home/carlos/Documentos/Pendientes';
const DIR_PROCESADOS = path.join(DIR_PENDIENTES, 'Procesados');
if (!fs.existsSync(DIR_PROCESADOS)) {
fs.mkdirSync(DIR_PROCESADOS);
}
function generateSlug(titulo) {
return titulo
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9\s-]/g, '')
.trim()
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.substring(0, 100);
}
function formatContent(text) {
const paragraphs = text.split(/\n+/);
const html = paragraphs
.map(p => p.trim())
.filter(p => p.length > 0)
.map(p => `<p>${p}</p>`)
.join('');
return html;
}
function extractSummary(text) {
const paragraphs = text.split(/\n+/).map(p => p.trim()).filter(p => p.length > 30);
if (paragraphs.length > 0) {
return paragraphs[0].substring(0, 200) + (paragraphs[0].length > 200 ? '...' : '');
}
return '';
}
function determineTipo(filename) {
const lower = filename.toLowerCase();
if (lower.includes('evento')) return 'evento';
if (lower.includes('aviso')) return 'aviso';
if (lower.includes('comunicado')) return 'comunicado';
if (lower.includes('galeria')) return 'galeria';
return 'noticia';
}
function formatTitle(filename) {
let base = path.basename(filename, path.extname(filename)).replace(/\(1\)/g, '').trim();
// Capitalize first letter of each word to look better
return base.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
}
function extractDate(filename, text) {
const months = {
'enero': 0, 'febrero': 1, 'marzo': 2, 'abril': 3, 'mayo': 4, 'junio': 5,
'julio': 6, 'agosto': 7, 'septiembre': 8, 'octubre': 9, 'noviembre': 10, 'diciembre': 11
};
// Attempt 1: From filename e.g. "28 DE MARZO DEL 2026"
const regex = /(\d{1,2})\s+DE\s+([A-Z]+)\s+(DEL?|DE)\s+(\d{4})/i;
let match = filename.match(regex);
if (!match) {
// Attempt 2: From the first few lines of text
const firstLines = text.split('\n').slice(0, 10).join(' ');
match = firstLines.match(regex);
}
if (match) {
const day = parseInt(match[1], 10);
const monthStr = match[2].toLowerCase();
const year = parseInt(match[4], 10);
if (months[monthStr] !== undefined) {
const date = new Date(year, months[monthStr], day, 12, 0, 0); // Noon to avoid timezone issues
return date.toISOString();
}
}
return new Date().toISOString(); // fallback
}
async function processFile(filePath, filename) {
const ext = path.extname(filename).toLowerCase();
let text = '';
try {
if (ext === '.pdf') {
const { execSync } = require('child_process');
text = execSync(`pdftotext "${filePath}" -`).toString();
} else if (ext === '.docx') {
const result = await mammoth.extractRawText({ path: filePath });
text = result.value;
} else if (ext === '.txt') {
text = fs.readFileSync(filePath, 'utf8');
} else {
console.log(`Skipping unsupported file: ${filename}`);
return false;
}
if (!text || text.trim().length === 0) {
console.log(`File empty or failed to extract: ${filename}`);
return false;
}
const titulo = formatTitle(filename);
const slug = generateSlug(titulo) + '-' + Math.random().toString(36).substring(2, 7);
const contenido = formatContent(text);
const tipo = determineTipo(filename);
const resumen_redes = extractSummary(text);
const docData = {
titulo,
slug,
contenido,
resumen_redes,
estado: 'publicado',
tipo_publicacion_id: defaultTipoPublicacionId,
fecha_publicacion: extractDate(filename, text),
};
await databases.createDocument(dbId, collectionId, ID.unique(), docData);
console.log(`✅ Publicado: ${titulo}`);
// Mover a procesados
fs.renameSync(filePath, path.join(DIR_PROCESADOS, filename));
return true;
} catch (error) {
console.error(`❌ Error procesando ${filename}:`, error.message);
return false;
}
}
async function main() {
const files = fs.readdirSync(DIR_PENDIENTES);
let successCount = 0;
for (const file of files) {
const filePath = path.join(DIR_PENDIENTES, file);
if (fs.statSync(filePath).isFile()) {
console.log(`Procesando ${file}...`);
const ok = await processFile(filePath, file);
if (ok) successCount++;
}
}
console.log(`\nImportación finalizada. ${successCount} publicaciones procesadas correctamente.`);
}
main();

View File

@ -5,11 +5,11 @@ async function setupLiveChat() {
console.log("Configuring Live Chat collection...");
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
const databases = new Databases(client);
const dbId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE;
const dbId = process.env.APPWRITE_DATABASE_ID;
try {
const collection = await databases.createCollection(

View File

@ -5,11 +5,11 @@ async function setupStreamState() {
console.log("Configuring Stream State collection...");
const client = new Client()
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT)
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
const databases = new Databases(client);
const dbId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE;
const dbId = process.env.APPWRITE_DATABASE_ID;
try {
const collection = await databases.createCollection(

View File

@ -91,11 +91,23 @@ export default async function EntityPage({
const relKey = field.relationCollection as string;
const relColId = collectionMap[relKey];
if (!relColId) return;
const ids = Array.from(new Set(
response.documents
.map(d => typeof d[field.name] === 'object' && d[field.name] !== null ? d[field.name].$id : d[field.name])
.filter(id => typeof id === 'string' && id.length > 0)
));
if (ids.length === 0) {
relationData[field.name] = [];
return;
}
try {
const relRes = await client.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
relColId,
[Query.limit(100)]
[Query.equal('$id', ids), Query.limit(100)]
);
relationData[field.name] = JSON.parse(JSON.stringify(relRes.documents));
} catch { }

View File

@ -77,9 +77,9 @@ export const PostCard: React.FC<PostCardProps> = ({
);
const postType =
typeof post.tipo_publicacion_id === 'object'
typeof post.tipo_publicacion_id === 'object' && post.tipo_publicacion_id !== null
? post.tipo_publicacion_id.descripcion
: post.tipo_publicacion_id;
: null;
const clickTimer = useRef<NodeJS.Timeout | null>(null);

View File

@ -226,18 +226,32 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
{filteredAndSortedPosts.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{filteredAndSortedPosts.map((post) => (
<PostCard
key={post.$id}
post={post}
userRole={userRole}
onEdit={handleEdit}
onDelete={handleDelete}
appwriteEndpoint={appwriteConfig.endpoint}
appwriteProjectId={appwriteConfig.projectId}
globalSettings={globalSettings}
/>
))}
{filteredAndSortedPosts.map((post) => {
const enrichedPost = { ...post };
if (typeof enrichedPost.imagen_destacada_id === 'string' && relationData['imagen_destacada_id']) {
const mediaDoc = relationData['imagen_destacada_id'].find(m => m.$id === enrichedPost.imagen_destacada_id);
if (mediaDoc) enrichedPost.imagen_destacada_id = mediaDoc as any;
}
if (typeof enrichedPost.tipo_publicacion_id === 'string' && relationData['tipo_publicacion_id']) {
const typeDoc = relationData['tipo_publicacion_id'].find(t => t.$id === enrichedPost.tipo_publicacion_id);
if (typeDoc) enrichedPost.tipo_publicacion_id = typeDoc as any;
}
return (
<PostCard
key={post.$id}
post={enrichedPost}
userRole={userRole}
onEdit={handleEdit}
onDelete={handleDelete}
appwriteEndpoint={appwriteConfig.endpoint}
appwriteProjectId={appwriteConfig.projectId}
globalSettings={globalSettings}
/>
);
})}
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4 py-20 border-2 border-dashed border-slate-800 rounded-2xl">

View File

@ -1,3 +0,0 @@
const DOMPurify = require('isomorphic-dompurify');
const txt = "Line 1\nLine 2\n\nLine 3\n<strong>Bold</strong> text";
console.log(DOMPurify.sanitize(txt));