diff --git a/src/app/actions/entities.ts b/src/app/actions/entities.ts
index cbd402e..e406fe1 100644
--- a/src/app/actions/entities.ts
+++ b/src/app/actions/entities.ts
@@ -4,6 +4,16 @@ import { collectionMap, SCHEMA_MAP } from '@/lib/schema';
import { createDocumentServer, updateDocumentServer } from './appwrite';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import DOMPurify from 'isomorphic-dompurify';
+
+// Re-usable rich text validator
+const safeHtmlSchema = z.string().transform(html =>
+ DOMPurify.sanitize(html, {
+ ADD_TAGS: ['iframe'],
+ ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']
+ })
+);
/**
* Centralized action to create a new entity document
@@ -31,6 +41,11 @@ export async function saveEntityAction(entity: string, formData: FormData) {
data.estado = data.estado.toLowerCase();
}
+ // Zod XSS Sanitization & Strict Types
+ if (entity === 'posts' && data.contenido) {
+ data.contenido = safeHtmlSchema.parse(data.contenido);
+ }
+
const res = await createDocumentServer(collectionId, data);
if (res.success) {
revalidatePath(`/dashboard/${entity}`);
@@ -66,6 +81,11 @@ export async function updateEntityAction(entity: string, id: string, formData: F
data.estado = data.estado.toLowerCase();
}
+ // Zod XSS Sanitization & Strict Types
+ if (entity === 'posts' && data.contenido) {
+ data.contenido = safeHtmlSchema.parse(data.contenido);
+ }
+
const res = await updateDocumentServer(collectionId, id, data);
if (res.success) {
revalidatePath(`/dashboard/${entity}`);
diff --git a/src/app/dashboard/[entity]/page.tsx b/src/app/dashboard/[entity]/page.tsx
index bcecf13..04d110a 100644
--- a/src/app/dashboard/[entity]/page.tsx
+++ b/src/app/dashboard/[entity]/page.tsx
@@ -22,8 +22,8 @@ export default async function EntityPage({
const q = sParams.q || '';
const page = Number(sParams.page) || 1;
const sort = sParams.sort || '$createdAt';
- const dir = sParams.dir === 'asc' ? 'asc' : 'desc';
- const limit = entity === 'posts' ? 12 : 15;
+ const dir: 'asc' | 'desc' = sParams.dir === 'asc' ? 'asc' : 'desc';
+ const limit: number = entity === 'posts' ? 12 : 15;
const offset = (page - 1) * limit;
const collectionId = collectionMap[entity];
@@ -169,25 +169,44 @@ export default async function EntityPage({
/>
)}
- {total > limit && (
-
-
- ANTERIOR
-
-
- Página {page} de {Math.ceil(total / limit)}
-
- = Math.ceil(total / limit) ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
- >
- SIGUIENTE
-
-
- )}
+ {total > limit && (() => {
+ const totalPages = Math.ceil(total / limit);
+ return (
+
+
+ « INICIO
+
+
+ ANTERIOR
+
+
+
+ Página {page} de {totalPages}
+
+
+ = totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
+ >
+ SIGUIENTE
+
+ = totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
+ title="Ir a la última página"
+ >
+ FINAL »
+
+
+ );
+ })()}
);
}
diff --git a/src/components/dashboard/MediaGridSelector.tsx b/src/components/dashboard/MediaGridSelector.tsx
index ef296e9..eab21f3 100644
--- a/src/components/dashboard/MediaGridSelector.tsx
+++ b/src/components/dashboard/MediaGridSelector.tsx
@@ -171,9 +171,7 @@ export const MediaGridSelector = ({
}`}
>
-
- {item.titulo}
-
+
{value === item.file_id && (
diff --git a/src/components/dashboard/RichTextEditor.tsx b/src/components/dashboard/RichTextEditor.tsx
index 40fbe27..3c2ff5e 100644
--- a/src/components/dashboard/RichTextEditor.tsx
+++ b/src/components/dashboard/RichTextEditor.tsx
@@ -250,11 +250,17 @@ export const RichTextEditor = ({ value, onChange, label }: RichTextEditorProps)
},
editorProps: {
attributes: {
- class: 'prose prose-invert max-w-none focus:outline-none min-h-[300px] p-6 text-slate-200 leading-relaxed',
+ class: 'prose prose-invert max-w-none focus:outline-none min-h-[300px] p-6 text-slate-200 leading-relaxed custom-scrollbar',
},
},
});
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
// Keep content synced if value changes externally
useEffect(() => {
if (editor && value !== editor.getHTML()) {
@@ -262,12 +268,23 @@ export const RichTextEditor = ({ value, onChange, label }: RichTextEditorProps)
}
}, [value, editor]);
+ if (!mounted) {
+ return (
+
+ );
+ }
+
return (
-
+
{/* Hidden input for form submission if needed, though we use controlled value */}
diff --git a/src/components/publications/PublicationLayout.tsx b/src/components/publications/PublicationLayout.tsx
index ce14fc9..aeb077e 100644
--- a/src/components/publications/PublicationLayout.tsx
+++ b/src/components/publications/PublicationLayout.tsx
@@ -266,8 +266,8 @@ export const PublicationLayout: React.FC
= ({
defaultValue={initialData?.resumen_redes || ''}
placeholder="Resumen corto para compartir en redes sociales..."
maxLength={500}
- className={textareaClass}
- style={{ minHeight: '80px' }}
+ className={`${textareaClass} custom-scrollbar resize-none`}
+ style={{ minHeight: '80px', maxHeight: '150px', overflowY: 'auto' }}
/>