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
+1013
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -11,6 +11,7 @@
"lint": "next lint"
},
"dependencies": {
"@uiw/react-md-editor": "^4.0.7",
"@yudiel/react-qr-scanner": "^2.2.1",
"axios": "^1.8.4",
"bootstrap": "^5.3.3",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,7 @@ import Button from '@/components/button';
import axiosInstance from '@/utils/api-config';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { getAxiosError } from '@/utils/errors-utils';
export default function Page() {
const [eventoBanner, setEventoBanner] = useState<File | null>(null);
@@ -69,6 +70,10 @@ export default function Page() {
console.log('Banner creado:', bannercreated.data);
}
console.log('Evento creado:', createEvento.data);
console.log('Datos del formulario:', datosFormulario);
const createFormulario = await axiosInstance.post('/cuestionario', {
id_evento: createEvento.data.id_evento,
...datosFormulario,
@@ -77,10 +82,11 @@ export default function Page() {
console.log('Formulario creado:', createFormulario.data);
}
router.push(`/administrador`)
router.push(`/administrador`);
} catch (error) {
const msg = getAxiosError(error);
toast.error(
'Error al crear el evento y el formulario. Por favor, inténtalo de nuevo.'
`Error al crear el evento o formulario: ${msg.message || 'Error desconocido'}`
);
console.error('Error al crear evento y formulario:', error);
}
@@ -1,10 +1,6 @@
'use client';
import MarkdownRenderer from '@/components/markdown-render';
import FormularioRegistro from '@/containers/formulario/formulario-registro';
import { useGetApi } from '@/hooks/use-get-api';
import FormularioPage from '@/containers/pages/formulario-page';
import { GetEventoWithCuestionario } from '@/types/evento';
import Image from 'next/image';
import { useParams } from 'next/navigation';
import { Metadata } from 'next';
import React from 'react';
type Params = {
@@ -13,36 +9,40 @@ type Params = {
id_cuestionario: string;
};
export default function Page() {
const params = useParams<Params>();
const { data, error } = useGetApi<GetEventoWithCuestionario>(
`/evento/${params.id_evento}/cuestionario/${params.id_cuestionario}`
type Props = {
params: Promise<Params>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id_evento, id_cuestionario } = await params;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/evento/${id_evento}/cuestionario/${id_cuestionario}`
);
if (error) {
return <div>Error al cargar el cuestionario: {error.message}</div>;
}
const data: GetEventoWithCuestionario = await response.json();
return {
title: data?.nombre_evento || 'Evento',
description: data?.descripcion_evento || 'Descripción del evento',
openGraph: {
title: data?.nombre_evento || 'Evento',
description: data?.descripcion_evento || 'Descripción del evento',
images: [
{
url: `${process.env.NEXT_PUBLIC_API_URL}/banners/${data.banner}`,
width: 800,
height: 400,
alt: data?.nombre_evento || 'Evento',
},
],
},
};
}
export default async function Page({ params }: Props) {
const { id_evento, id_cuestionario } = await params;
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>
<FormularioPage id_evento={id_evento} id_cuestionario={id_cuestionario} />
);
}
+2 -4
View File
@@ -9,11 +9,9 @@ export interface CarouselProps {
}[];
}
export default function Carousel({ id, images }: CarouselProps) {
console.log('Carousel images:', images);
export default function Carousel({ id="carousel-banners-eventos", images }: CarouselProps) {
if (!images || images.length === 0) {
return <div>No hay imágenes para mostrar</div>;
return <div></div>;
}
return (
+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>
);
}
+12 -2
View File
@@ -13,7 +13,6 @@ export const plantillasDisponibles: Plantilla[] = [
nombre: 'Registro Comunidad Trabajador',
id: 'registro-comunidad-estudiantil',
datos: {
id_evento: 1,
nombre_form: 'Registro',
descripcion:
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
@@ -78,7 +77,6 @@ export const plantillasDisponibles: Plantilla[] = [
nombre: 'Registro Comunidad Alumnos',
id: 'registro-comunidad-alumnos',
datos: {
id_evento: 1,
nombre_form: 'Registro',
descripcion:
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
@@ -151,3 +149,15 @@ export const plantillasDisponibles: Plantilla[] = [
},
},
];
/*
¡Bienvenidas y bienvenidos a la Facultad!
Inicia esta nueva etapa con un recorrido especial por nuestras instalaciones, pensado para que conozcas tu nueva casa universitaria. Te acompañaremos por aulas, laboratorios, bibliotecas, áreas comunes y servicios que estarán a tu disposición durante tu formación. Además, podrás resolver dudas y conocer a quienes serán parte de tu camino académico.
¡No faltes, esta actividad marcará el inicio de grandes experiencias!
*/
/*
Recorrido de bienvenida para nuevo personal
Damos la más cordial bienvenida al personal de nuevo ingreso a nuestra Facultad. Este recorrido está diseñado para familiarizarlos con las principales áreas administrativas, académicas y de servicios. También tendrán la oportunidad de conocer al equipo de trabajo y obtener información clave para integrarse con éxito a la comunidad universitaria.
¡Les esperamos con gusto para comenzar juntos esta nueva etapa profesional!
*/
-1
View File
@@ -22,7 +22,6 @@ export type TipoPregunta =
| 'Multiple';
export interface FormularioCreacion {
id_evento: number;
nombre_form: string;
descripcion: string;
fecha_inicio: string; // ISO 8601 date string