From ae4e242252311ad7d1e48824c707aea056ec490e Mon Sep 17 00:00:00 2001 From: "Carlos (Almeira)" Date: Sat, 28 Mar 2026 17:09:35 -0500 Subject: [PATCH] refactor: UX enhancements, SEO automation, Entity Singleton and Media URLs --- src/app/actions/entity.ts | 20 +++++++-- src/app/clasificados/page.tsx | 2 +- src/app/dashboard/settings/page.tsx | 3 +- src/app/eventos/page.tsx | 2 +- src/app/page.tsx | 34 ++++++++------- src/components/NewsCard.tsx | 18 +++++++- .../dashboard/MediaGridSelector.tsx | 32 ++++++++++++-- .../publications/PublicationLayout.tsx | 43 ++++++++++++++++--- .../publications/PublicationsList.tsx | 9 ++-- 9 files changed, 127 insertions(+), 36 deletions(-) diff --git a/src/app/actions/entity.ts b/src/app/actions/entity.ts index 0a9f363..12ef12f 100644 --- a/src/app/actions/entity.ts +++ b/src/app/actions/entity.ts @@ -44,17 +44,29 @@ export async function updateEntityConfig(data: any) { const databases = await getAdminClient(); const existing = await getEntityConfig(); - if (!data || Object.keys(data).length === 0) { + let parsedData = data; + if (typeof FormData !== 'undefined' && data instanceof FormData) { + parsedData = {}; + data.forEach((value, key) => { + if (key !== '$id' && key !== '$collectionId' && key !== '$databaseId') { + parsedData[key] = value; + } + }); + } + + if (!parsedData || Object.keys(parsedData).length === 0) { console.warn('updateEntityConfig: Data is empty, skipping update.'); return existing; } + const SINGLETON_ID = existing?.$id || 'main_profile_singleton'; + if (existing) { const result = await databases.updateDocument( DATABASE_ID, COLLECTION_ID, existing.$id, - data + parsedData ); revalidatePath('/'); return result; @@ -62,8 +74,8 @@ export async function updateEntityConfig(data: any) { const result = await databases.createDocument( DATABASE_ID, COLLECTION_ID, - ID.unique(), - data + SINGLETON_ID, + parsedData ); revalidatePath('/'); return result; diff --git a/src/app/clasificados/page.tsx b/src/app/clasificados/page.tsx index 1b64727..322ca0c 100644 --- a/src/app/clasificados/page.tsx +++ b/src/app/clasificados/page.tsx @@ -23,7 +23,7 @@ async function fetchClasificadosData() { const postsCount = settings.feed_news_count || 12; const cardSettings = { showCategory: settings.news_card_show_category ?? false, - showFooterDate: settings.show_news_date ?? false, + showFooterDate: String(settings.show_news_date) === 'true', showFooterButton: settings.news_card_show_footer_button ?? true, footerText: settings.news_card_footer_text || "" }; diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 805b39e..4e7b38f 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -34,11 +34,12 @@ export default async function SettingsPage() { const sessionClient = await createSessionClient(); if (!sessionClient) throw new Error("Acceso denegado"); + const showNewsDateVal = formData.get('show_news_date'); const updateData = { ticker_count: parseInt(formData.get('ticker_count') as string), feed_news_count: parseInt(formData.get('feed_news_count') as string), rotation_seconds: parseInt(formData.get('rotation_seconds') as string), - show_news_date: formData.get('show_news_date') === 'true' + show_news_date: showNewsDateVal === 'true' || showNewsDateVal === 'on' }; if (formData.get('docId')) { diff --git a/src/app/eventos/page.tsx b/src/app/eventos/page.tsx index 18d0462..139ff80 100644 --- a/src/app/eventos/page.tsx +++ b/src/app/eventos/page.tsx @@ -23,7 +23,7 @@ async function fetchEventosData() { const postsCount = settings.feed_news_count || 12; const cardSettings = { showCategory: settings.news_card_show_category ?? false, - showFooterDate: settings.show_news_date ?? false, + showFooterDate: String(settings.show_news_date) === 'true', showFooterButton: settings.news_card_show_footer_button ?? true, footerText: settings.news_card_footer_text || "" }; diff --git a/src/app/page.tsx b/src/app/page.tsx index b5acb67..9d53dbc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; -import { createAdminClient } from '@/lib/appwrite.server'; +import { createAdminClient, getLoggedInUser } from '@/lib/appwrite.server'; import { Query } from 'node-appwrite'; import { NewsCard } from '@/components/NewsCard'; import { GlobalSidebar, hasSidebarContent } from '@/components/AdWrappers'; @@ -25,7 +25,7 @@ async function fetchHomeData() { const postsCount = settings.feed_news_count || 12; const cardSettings = { showCategory: settings.news_card_show_category ?? false, - showFooterDate: settings.show_news_date ?? false, + showFooterDate: String(settings.show_news_date) === 'true', showFooterButton: settings.news_card_show_footer_button ?? true, footerText: settings.news_card_footer_text || "" }; @@ -72,6 +72,9 @@ async function fetchHomeData() { export default async function Home() { const { posts, cardSettings } = await fetchHomeData(); const hasSidebar = await hasSidebarContent(); + + const user = await getLoggedInUser(); + const isAdmin = (user as any)?.labels?.includes('admin') || (user as any)?.labels?.includes('editor') || user?.$id === 'admin-emergency'; return (
@@ -150,19 +153,20 @@ export default async function Home() { return ( ]*>?/gm, '').substring(0, 150) + '...')} - tipo="Noticia" - fecha={post.fecha_publicacion || post.$createdAt} - imagenUrl={imagenUrl} - showCategory={cardSettings.showCategory} - showFooterDate={cardSettings.showFooterDate} - showFooterButton={cardSettings.showFooterButton} - footerText={cardSettings.footerText} - /> + key={`post-${post.$id}`} + id={post.$id} + slug={post.slug || post.$id} + titulo={post.titulo} + resumen={post.resumen_redes || post.contenido || ''} + tipo={typeof post.tipo_publicacion_id === 'object' ? post.tipo_publicacion_id.descripcion : ''} + fecha={post.fecha_publicacion || post.$createdAt} + imagenUrl={imagenUrl} + showCategory={cardSettings.showCategory} + showFooterDate={cardSettings.showFooterDate} + showFooterButton={cardSettings.showFooterButton} + footerText={cardSettings.footerText} + isAdmin={isAdmin} + /> ) })} diff --git a/src/components/NewsCard.tsx b/src/components/NewsCard.tsx index fd98afd..180dcb4 100644 --- a/src/components/NewsCard.tsx +++ b/src/components/NewsCard.tsx @@ -14,6 +14,7 @@ interface NewsCardProps { showFooterDate?: boolean; showFooterButton?: boolean; footerText?: string; + isAdmin?: boolean; } export const NewsCard = ({ @@ -27,7 +28,8 @@ export const NewsCard = ({ showCategory = false, showFooterDate = false, showFooterButton = false, - footerText + footerText, + isAdmin = false }: NewsCardProps) => { const titulo = decodeHtmlEntities(rawTitulo); const resumen = decodeHtmlEntities(rawResumen); @@ -57,6 +59,20 @@ export const NewsCard = ({ )} + + {/* Edit Button for Admins */} + {isAdmin && ( +
+ e.stopPropagation()} + className="flex items-center gap-2 px-3 py-1.5 rounded-full text-[10px] font-black uppercase tracking-wider bg-slate-950/80 backdrop-blur border border-cyan-500/50 text-cyan-400 hover:bg-cyan-500 hover:text-slate-950 transition-all shadow-lg" + title="Editar en Dashboard" + > + Editar + +
+ )} {/* Contenido */} diff --git a/src/components/dashboard/MediaGridSelector.tsx b/src/components/dashboard/MediaGridSelector.tsx index eab21f3..7ddfa6a 100644 --- a/src/components/dashboard/MediaGridSelector.tsx +++ b/src/components/dashboard/MediaGridSelector.tsx @@ -59,6 +59,20 @@ export const MediaGridSelector = ({ } }; + useEffect(() => { + // If we have a value but haven't loaded its document, fetch just that one + if (value && mode === 'field' && !mediaItems.some(m => m.$id === value)) { + databases.getDocument(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', value) + .then(doc => { + setMediaItems(prev => { + // Protect against race conditions duplicating the doc + if (prev.some(m => m.$id === value)) return prev; + return [...prev, doc]; + }); + }).catch(err => console.error("Error resolver media thumbnail:", err)); + } + }, [value, mode]); + const handleUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -80,7 +94,7 @@ export const MediaGridSelector = ({ // 3. Update state setMediaItems(prev => [doc, ...prev]); const url = getAppwriteImageUrl(uploadedFile.$id, 1200, 800); - onChange(uploadedFile.$id, url); + onChange(doc.$id, url); closeModal(); } catch (error) { console.error("Error upload:", error); @@ -162,7 +176,7 @@ export const MediaGridSelector = ({
{ - onChange(item.file_id, getAppwriteImageUrl(item.file_id, 1200, 800)); + onChange(item.$id, getAppwriteImageUrl(item.file_id, 1200, 800)); if (mode !== 'modal') setIsOpen(false); closeModal(); }} @@ -172,7 +186,7 @@ export const MediaGridSelector = ({ > {item.titulo}
- {value === item.file_id && ( + {value === item.$id && (
@@ -224,7 +238,17 @@ export const MediaGridSelector = ({ > {value ? ( <> - Selected Preview + {(() => { + // Si value existe, buscamos su documento para sacar el file_id + const selectedDoc = mediaItems.find(m => m.$id === value); + // Si aún no está en la lista (cargando), mostramos loader genérico + if (!selectedDoc && mediaItems.length > 0) return null; // No está en la DB o se paginó, idealmente pasamos un initialUrl. + const fileIdToRender = selectedDoc ? selectedDoc.file_id : value; // Fallback al propio ID por si es legacy. + + return ( + Selected Preview + ); + })()}
Cambiar Imagen ↔ diff --git a/src/components/publications/PublicationLayout.tsx b/src/components/publications/PublicationLayout.tsx index aeb077e..7df905c 100644 --- a/src/components/publications/PublicationLayout.tsx +++ b/src/components/publications/PublicationLayout.tsx @@ -96,11 +96,35 @@ export const PublicationLayout: React.FC = ({ const [slug, setSlug] = useState(initialData?.slug || ''); const [isAutoSlug, setIsAutoSlug] = useState(!initialData?.slug); const [contenido, setContenido] = useState(initialData?.contenido || ''); - const [imagenDestacadaId, setImagenDestacadaId] = useState( + const [resumen_redes, setResumenRedes] = useState(initialData?.resumen_redes || ''); + const [imagenDestacadaId, setImagenDestacadaId] = useState( typeof initialData?.imagen_destacada_id === 'object' - ? initialData.imagen_destacada_id.$id + ? initialData?.imagen_destacada_id?.$id : initialData?.imagen_destacada_id || '' ); + + // SEO Smart Autofill States + const [seoTitulo, setSeoTitulo] = useState(initialData?.seo_titulo || ''); + const [seoDescripcion, setSeoDescripcion] = useState(initialData?.seo_descripcion || ''); + const [seoDirty, setSeoDirty] = useState({ titulo: !!initialData?.seo_titulo, descripcion: !!initialData?.seo_descripcion }); + + useEffect(() => { + if (!seoDirty.titulo && titulo) { + setSeoTitulo(titulo.substring(0, 70)); + } + }, [titulo, seoDirty.titulo]); + + useEffect(() => { + if (!seoDirty.descripcion) { + // Strip HTML if it's the rich text + const richTextSnippet = contenido?.replace(/<[^>]*>?/gm, '').substring(0, 160) || ''; + const sourceMeta = resumen_redes || richTextSnippet; + if (sourceMeta) { + setSeoDescripcion(sourceMeta.substring(0, 160)); + } + } + }, [contenido, resumen_redes, seoDirty.descripcion]); + const [validationErrors, setValidationErrors] = useState>({}); // Populate relation cache in the store on mount @@ -263,7 +287,8 @@ export const PublicationLayout: React.FC = ({ >