feat: add markdown editor for event description and update event forms

- Added @uiw/react-md-editor dependency for rich text editing.
- Implemented dynamic loading of the markdown editor in create and edit event forms.
- Updated event forms to include new fields for 'apellidos', 'genero', and 'rfc'.
- Refactored event and questionnaire pages to improve data fetching and rendering.
- Removed unnecessary Excel files from the public directory.
- Enhanced error handling and logging in event creation and form submission processes.
- Updated carousel component to handle empty image arrays gracefully.
- Created a new FormularioPage component to streamline questionnaire rendering.
This commit is contained in:
miguel
2025-06-18 15:51:17 -06:00
parent ea9f3b72f7
commit e092eeec4d
20 changed files with 1195 additions and 79 deletions
+27 -12
View File
@@ -1,9 +1,16 @@
'use client';
import ImageUploader from '@/components/banner-uploader';
import Input from '@/components/input';
import Select from '@/components/select';
import { CreateEventoType } from '@/types/create-evento';
import { commands } from '@uiw/react-md-editor';
import dynamic from 'next/dynamic';
import React from 'react';
// Carga dinámica para evitar problemas con SSR
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
const MAX_LENGTH = 500;
interface CreateEventoProps {
evento: CreateEventoType;
handleChange: (field: keyof CreateEventoType, value: string | Date) => void;
@@ -17,13 +24,14 @@ export default function CreateEvento({
}: CreateEventoProps) {
return (
<div className="row border p-3 rounded my-4">
<h2>
Informacion del Evento
</h2>
<h2>Información del Evento</h2>
<p>
Completa la siguiente informacion para describir la informacion general del evento
Completa la siguiente información para describir los detalles generales
del evento.
</p>
<ImageUploader label="Banner del evento" onChange={handleBannerChange} />
<Input
label="Nombre del evento"
required
@@ -31,15 +39,21 @@ export default function CreateEvento({
onChange={(e) => handleChange('nombre_evento', e.target.value)}
/>
<div className="mb-3">
<div className="mb-4">
<label className="form-label">Descripción del evento</label>
<textarea
className="form-control bg-white"
rows={3}
placeholder="Describe el evento..."
value={evento.descripcion_evento}
onChange={(e) => handleChange('descripcion_evento', e.target.value)}
/>
<div data-color-mode="light">
<MDEditor
value={evento.descripcion_evento}
onChange={(value) => {
const texto = value || '';
if (texto.length <= MAX_LENGTH) {
handleChange('descripcion_evento', texto);
}
}}
height={300}
commands={[commands.bold, commands.italic, commands.hr]}
/>
</div>
</div>
<Select
@@ -66,6 +80,7 @@ export default function CreateEvento({
}
/>
</div>
<div className="col-md-6">
<Input
label="Fecha de fin"
+3
View File
@@ -25,12 +25,15 @@ const tiposValidacion: TiposValidacion[] = [
'correo_institucional',
'telefono',
'nombre',
'apellidos',
'genero',
'entero',
'decimal',
'comunidad_alumno',
'cuenta_alumno',
'comunidad_trabajador',
'cuenta_trabajador',
'rfc'
];
type CampoPreguntaSimple =
+17 -8
View File
@@ -7,8 +7,11 @@ import { CreateEventoType } from '@/types/create-evento';
import { GetEventoWithCuestionario } from '@/types/evento';
import axiosInstance from '@/utils/api-config';
import { getAxiosError } from '@/utils/errors-utils';
import { commands } from '@uiw/react-md-editor';
import dynamic from 'next/dynamic';
import React, { useState } from 'react';
import toast from 'react-hot-toast';
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });
interface EditEventoProps {
evento: GetEventoWithCuestionario;
@@ -88,15 +91,21 @@ export default function EditEvento({
onChange={(e) => handleChange('nombre_evento', e.target.value)}
/>
<div className="mb-3">
<div className="mb-4">
<label className="form-label">Descripción del evento</label>
<textarea
className="form-control bg-white"
rows={5}
placeholder="Describe el evento..."
value={evento.descripcion_evento}
onChange={(e) => handleChange('descripcion_evento', e.target.value)}
/>
<div data-color-mode="light">
<MDEditor
value={evento.descripcion_evento}
onChange={(value) => {
const texto = value || '';
if (texto.length <= 500) {
handleChange('descripcion_evento', texto);
}
}}
height={250}
commands={[commands.bold, commands.italic, commands.hr]}
/>
</div>
</div>
<Select
@@ -12,11 +12,12 @@ import { useRouter } from 'next/navigation';
import { getAxiosError } from '@/utils/errors-utils';
type UsuarioData = {
nombre: string;
apellidos: string;
correo: string;
genero: 'M' | 'F';
carrera: string;
nombre: string; // alumno o trabajador
apellidos: string; // alumno o trabajador
correo: string; // alumno o trabajador (en los alumnos se usa el id_ncuenta en trabajadores ellos deben escribirlo)
genero: 'M' | 'F'; // alumno o trabajador
carrera: string; // solo para alumnos
rfc?: string; // solo para trabajadores se usa para buscar información de cuenta
};
const confirmativeOption = ['Sí', 'Si', 'Si, claro', 'Claro que sí'];
@@ -60,7 +61,8 @@ export default function FormularioRegistro({
const preguntaCuenta = preguntas?.find(
(pregunta) =>
pregunta.validacion === 'cuenta_alumno' ||
pregunta.validacion === 'cuenta_trabajador'
pregunta.validacion === 'cuenta_trabajador' ||
pregunta.validacion === 'rfc'
);
// ------------------------
@@ -71,12 +73,13 @@ export default function FormularioRegistro({
const esCuentaAlumno = preguntaCuenta?.validacion === 'cuenta_alumno';
const esCuentaTrabajador =
preguntaCuenta?.validacion === 'cuenta_trabajador';
preguntaCuenta?.validacion === 'cuenta_trabajador' ||
preguntaCuenta?.validacion === 'rfc';
const puedeBuscar =
cuenta &&
typeof cuenta === 'string' &&
((esCuentaAlumno && cuenta.length === 9) || esCuentaTrabajador);
((esCuentaAlumno && cuenta.length === 9) || (esCuentaTrabajador && cuenta.length === 5));
const fetchCuentaInfo = async () => {
try {
@@ -85,17 +88,22 @@ export default function FormularioRegistro({
: `/trabajadores/${cuenta}`;
const res = await axiosInstance.get(endpoint);
console.log('Datos de cuenta obtenidos:', res.data);
// Usar datos fake para pruebas
// Usar datos fake para pruebas
setCuentaInfo({
nombre: res.data.nombre,
apellidos: res.data.apellidos,
correo: '',
genero: res.data.genero,
carrera: res.data.carrera ? res.data.carrera : '',
});
// Usar datos fake para pruebas
/* setCuentaInfo({
nombre: 'Juan',
apellidos: 'Pérez',
correo: 'juan.perez@pcpuma.acatlan.unam.mx',
genero: 'M',
carrera: 'Ingeniería en Computación',
});
}); */
} catch (error) {
console.error('Error al obtener datos de cuenta:', error);
setCuentaInfo(null);
@@ -358,6 +366,7 @@ export default function FormularioRegistro({
cuentaInfo={cuentaInfo}
isComunidad={isComunidad?.label}
onChange={actualizarRespuesta}
preguntaComunidad={preguntaComunidad?.validacion ?? ''}
/>
);
}
@@ -17,6 +17,7 @@ type Props = {
} | null;
isComunidad: string | undefined;
onChange: (id: string, valor: string) => void;
preguntaComunidad: string;
};
const confirmativeOption = ['Sí', 'Si', 'Si, claro', 'Claro que sí'];
@@ -30,28 +31,32 @@ export default function PrefetchAbiertaCorta({
isComunidad,
obligatorio,
onChange,
preguntaComunidad,
}: Props) {
console.log(preguntaComunidad);
let defaultValue = '';
let disabled = false;
if (confirmativeOption.includes(isComunidad ?? '') && cuentaInfo) {
if (validacion === 'nombre') {
if (validacion === 'nombre' && cuentaInfo.nombre) {
defaultValue = cuentaInfo.nombre;
disabled = true;
}
if (validacion === 'apellidos') {
if (validacion === 'apellidos' && cuentaInfo.apellidos) {
defaultValue = cuentaInfo.apellidos;
disabled = true;
}
if (validacion === 'correo') {
if (validacion === 'correo' && cuentaInfo.correo) {
defaultValue = cuentaInfo.correo;
disabled = true;
if (preguntaComunidad !== 'comunidad_trabajador') {
disabled = true;
}
}
if (validacion === 'institucion') {
defaultValue = 'FES Acatlán';
disabled = true;
}
if (validacion === 'carrera') {
if (validacion === 'carrera' && cuentaInfo.carrera) {
defaultValue = cuentaInfo.carrera;
disabled = true;
}
+48
View File
@@ -0,0 +1,48 @@
'use client'
import MarkdownRenderer from '@/components/markdown-render';
import { useGetApi } from '@/hooks/use-get-api';
import Image from 'next/image';
import React from 'react';
import FormularioRegistro from '../formulario/formulario-registro';
import { GetEventoWithCuestionario } from '@/types/evento';
export default function FormularioPage({
id_evento,
id_cuestionario,
}: {
id_evento: string;
id_cuestionario: string;
}) {
const { data, error } = useGetApi<GetEventoWithCuestionario>(
`/evento/${id_evento}/cuestionario/${id_cuestionario}`
);
if (error) {
return <div>Error al cargar el cuestionario: {error.message}</div>;
}
return (
<div className="container my-5">
<div className="text-center mb-4">
{data?.banner && (
<Image
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${data.banner}`}
alt={data?.nombre_evento || ''}
width={800}
height={400}
className="img-fluid rounded"
/>
)}
</div>
<h1>{data?.nombre_evento}</h1>
{data?.descripcion_evento && (
<MarkdownRenderer markdown={data?.descripcion_evento} />
)}
{data && (
<FormularioRegistro
id_cuestionario={data?.cuestionario.id_cuestionario}
/>
)}
</div>
);
}