feat: Implement event and questionnaire management features
- Added event creation page with form handling and validation. - Introduced layout for event management with context provider. - Created event listing page with active and recent events display. - Developed questionnaire creation page linked to events. - Implemented breadcrumb navigation for better user experience. - Added reusable components for event and questionnaire cards. - Integrated API calls for fetching and managing event data. - Enhanced slugify utility for better URL handling. - Established context for managing event state across components.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
@@ -1,161 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import { FormularioCreacion } from '@/types/create-formulario';
|
||||
import CreateEvento from '@/containers/create-evento';
|
||||
import { plantillasDisponibles } from '@/data/plantillas';
|
||||
import Image from 'next/image';
|
||||
import CreateFormulario from '@/containers/create-formulario';
|
||||
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);
|
||||
const [evento, setEvento] = useState<CreateEventoType>({
|
||||
tipo_evento: '',
|
||||
nombre_evento: '',
|
||||
descripcion_evento: '',
|
||||
fecha_inicio: new Date(),
|
||||
fecha_fin: new Date(),
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
const [datosFormulario, setDatosFormulario] =
|
||||
useState<FormularioCreacion | null>(null);
|
||||
|
||||
const handleChange = (
|
||||
field: keyof CreateEventoType,
|
||||
value: string | Date
|
||||
) => {
|
||||
setEvento((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBannerChange = (file: File | null) => {
|
||||
setEventoBanner(file);
|
||||
};
|
||||
|
||||
const handleSeleccionarPlantilla = (plantillaId: string) => {
|
||||
const seleccionada = plantillasDisponibles.find(
|
||||
(p) => p.id === plantillaId
|
||||
);
|
||||
if (seleccionada) {
|
||||
setDatosFormulario(seleccionada.datos);
|
||||
console.log('Plantilla seleccionada:', seleccionada);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCrearEventoYFormulario = async () => {
|
||||
try {
|
||||
const createEventoWithCuestionario = await axiosInstance.post('/cuestionario/withEvento', {
|
||||
evento: evento,
|
||||
cuestionario: datosFormulario,
|
||||
});
|
||||
|
||||
if (createEventoWithCuestionario) {
|
||||
const formData = new FormData();
|
||||
if (eventoBanner) {
|
||||
formData.append('banner', eventoBanner);
|
||||
const bannercreated = await axiosInstance.post(
|
||||
`/evento/${createEventoWithCuestionario.data.id_evento}/banner`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log('Banner creado:', bannercreated.data);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Evento y formulario creados:', createEventoWithCuestionario.data);
|
||||
|
||||
toast.success('Evento y formulario creados exitosamente');
|
||||
router.push(`/administrador`);
|
||||
} catch (error) {
|
||||
const msg = getAxiosError(error);
|
||||
toast.error(
|
||||
`Error al crear el evento o formulario: ${msg.message || 'Error desconocido'}`
|
||||
);
|
||||
console.error('Error al crear evento y formulario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="my-4">
|
||||
<h1>Crear evento y formulario de registro</h1>
|
||||
<p>
|
||||
En esta pagina podras crear un evento y un primer formulario de
|
||||
registro, posteriormente podras crear mas formularios relacionados a
|
||||
este evento.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CreateEvento
|
||||
evento={evento}
|
||||
handleChange={handleChange}
|
||||
handleBannerChange={handleBannerChange}
|
||||
/>
|
||||
|
||||
{!datosFormulario && (
|
||||
<div className="border p-3 rounded my-4">
|
||||
<h2 className="h5 fw-bold mb-2">
|
||||
Selecciona una plantilla para el formulario:
|
||||
</h2>
|
||||
<p className="text-secondary small lh-sm">
|
||||
Elige una de las plantillas disponibles para crear tu formulario de
|
||||
registro.
|
||||
<br />
|
||||
Estas plantillas están diseñadas para garantizar la correcta
|
||||
integración con el sistema, permitiendo que los datos se obtengan y
|
||||
se prellenan automáticamente de forma precisa.
|
||||
<br />
|
||||
<span className="text-danger fw-semibold">
|
||||
Para asegurar el funcionamiento óptimo del prellenado, modifica la
|
||||
estructura solo si es necesario.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="d-flex gap-4 flex-wrap">
|
||||
{plantillasDisponibles.map((plantilla) => (
|
||||
<div
|
||||
className="mb-3 cursor-pointer"
|
||||
key={plantilla.id}
|
||||
onClick={() => handleSeleccionarPlantilla(plantilla.id)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Image
|
||||
src={plantilla.imagen}
|
||||
width={200}
|
||||
height={200}
|
||||
alt={`Plantilla de formulario ${plantilla.nombre}`}
|
||||
className="img-fluid rounded shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{datosFormulario && (
|
||||
<CreateFormulario
|
||||
formulario={datosFormulario}
|
||||
onChange={(nuevo) => setDatosFormulario({ ...nuevo })}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button className="mb-4" onClick={handleCrearEventoYFormulario}>
|
||||
Crear Evento y Formulario
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+13
-10
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
import SimpleInput from '@/components/input';
|
||||
import Table, { Header } from '@/components/table';
|
||||
import EditFormulario from '@/containers/edit-formulario';
|
||||
@@ -6,10 +7,10 @@ import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetCuestionario } from '@/types/cuestionario';
|
||||
import { ParticipacionEvento } from '@/types/participante-evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React, { useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useEvento } from '@/context/evento';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
@@ -18,6 +19,7 @@ type Params = {
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<Params>();
|
||||
const { evento } = useEvento();
|
||||
|
||||
const [cuestionario, setCuestionario] =
|
||||
React.useState<GetCuestionario | null>(null);
|
||||
@@ -140,17 +142,18 @@ export default function Page() {
|
||||
p.participante.correo.toLowerCase().includes(busquedaCorreo.toLowerCase())
|
||||
);
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: 'Inicio', href: '/administrador/eventos' },
|
||||
{
|
||||
label: evento?.nombre_evento || 'Evento',
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
},
|
||||
{ label: cuestionario?.nombre_form || 'Formulario' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Link
|
||||
href={`/administrador/evento/${params.id_evento}`}
|
||||
className="btn btn-link mb-3"
|
||||
>
|
||||
<i className="bi bi-arrow-left me-2"></i>
|
||||
Volver
|
||||
</Link>
|
||||
|
||||
<h2 className="mb-4">Formulario</h2>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
{cuestionario && (
|
||||
<EditFormulario
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client';
|
||||
import { plantillasDisponibles } from '@/data/plantillas';
|
||||
import { FormularioCreacion } from '@/types/create-formulario';
|
||||
import Image from 'next/image';
|
||||
import React, { useState } from 'react';
|
||||
import CreateFormulario from '@/containers/create-formulario';
|
||||
import Button from '@/components/button';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
import { useEvento } from '@/context/evento';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<Params>();
|
||||
const router = useRouter();
|
||||
const { evento, refetch } = useEvento();
|
||||
const [datosFormulario, setDatosFormulario] =
|
||||
useState<FormularioCreacion | null>(null);
|
||||
|
||||
const handleSeleccionarPlantilla = (plantillaId: string) => {
|
||||
const seleccionada = plantillasDisponibles.find(
|
||||
(p) => p.id === plantillaId
|
||||
);
|
||||
if (seleccionada) {
|
||||
setDatosFormulario(seleccionada.datos);
|
||||
console.log('Plantilla seleccionada:', seleccionada);
|
||||
}
|
||||
};
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ label: 'Inicio', href: '/administrador/eventos' },
|
||||
{
|
||||
label: evento?.nombre_evento || 'Evento',
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
},
|
||||
{ label: 'Crear formulario' },
|
||||
];
|
||||
|
||||
const handleCrearEvento = async () => {
|
||||
if (!datosFormulario || !evento) {
|
||||
toast.error('Faltan datos para crear el formulario');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar fechas del formulario respecto al evento
|
||||
const fechaInicioFormulario = new Date(datosFormulario.fecha_inicio);
|
||||
const fechaFinFormulario = new Date(datosFormulario.fecha_fin);
|
||||
const fechaInicioEvento = new Date(evento.fecha_inicio);
|
||||
const fechaFinEvento = new Date(evento.fecha_fin);
|
||||
|
||||
// Convertir a solo fecha (sin hora) para comparación
|
||||
const fechaInicioFormularioSoloFecha = new Date(
|
||||
fechaInicioFormulario.getFullYear(),
|
||||
fechaInicioFormulario.getMonth(),
|
||||
fechaInicioFormulario.getDate()
|
||||
);
|
||||
const fechaFinFormularioSoloFecha = new Date(
|
||||
fechaFinFormulario.getFullYear(),
|
||||
fechaFinFormulario.getMonth(),
|
||||
fechaFinFormulario.getDate()
|
||||
);
|
||||
const fechaInicioEventoSoloFecha = new Date(
|
||||
fechaInicioEvento.getFullYear(),
|
||||
fechaInicioEvento.getMonth(),
|
||||
fechaInicioEvento.getDate()
|
||||
);
|
||||
const fechaFinEventoSoloFecha = new Date(
|
||||
fechaFinEvento.getFullYear(),
|
||||
fechaFinEvento.getMonth(),
|
||||
fechaFinEvento.getDate()
|
||||
);
|
||||
|
||||
console.log('Fechas del formulario:', {
|
||||
inicio: fechaInicioFormulario,
|
||||
fin: fechaFinFormulario,
|
||||
});
|
||||
console.log('Fechas del evento:', {
|
||||
inicio: fechaInicioEvento,
|
||||
fin: fechaFinEvento,
|
||||
});
|
||||
|
||||
if (fechaInicioFormularioSoloFecha < fechaInicioEventoSoloFecha) {
|
||||
toast.error(
|
||||
'La fecha de inicio del formulario debe ser posterior o igual a la fecha de inicio del evento'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fechaFinFormularioSoloFecha > fechaFinEventoSoloFecha) {
|
||||
toast.error(
|
||||
'La fecha de fin del formulario debe ser anterior o igual a la fecha de fin del evento'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fechaInicioFormulario > fechaFinFormulario) {
|
||||
toast.error(
|
||||
'La fecha de inicio del formulario debe ser anterior a la fecha de fin'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.post(`/cuestionario`, {
|
||||
id_evento: evento?.id_evento || params.id_evento,
|
||||
...datosFormulario,
|
||||
});
|
||||
console.log('Formulario creado:', res.data);
|
||||
refetch();
|
||||
toast.success('Formulario creado exitosamente');
|
||||
router.push(`/administrador/evento/${params.id_evento}`);
|
||||
} catch (error) {
|
||||
const msg = getAxiosError(error);
|
||||
toast.error(msg.message || 'Error al crear el formulario');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<div className="p-4 border rounded bg-light mt-2">
|
||||
<h2 className="mb-2">Creación de formulario</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Esta sección te permite crear y configurar sub-eventos en forma de
|
||||
formularios asociados al evento. Selecciona una plantilla compatible,
|
||||
ajusta los campos necesarios y publícalo para habilitar el registro,
|
||||
encuestas o confirmaciones dentro de este evento.
|
||||
</p>
|
||||
</div>
|
||||
{!datosFormulario && (
|
||||
<div className="border p-3 rounded my-4">
|
||||
<h2 className="h5 fw-bold mb-2">
|
||||
Selecciona una plantilla para el formulario:
|
||||
</h2>
|
||||
<p className="text-secondary small lh-sm">
|
||||
Elige una de las plantillas disponibles para crear tu formulario de
|
||||
registro.
|
||||
<br />
|
||||
Estas plantillas están diseñadas para garantizar la correcta
|
||||
integración con el sistema, permitiendo que los datos se obtengan y
|
||||
se prellenan automáticamente de forma precisa.
|
||||
<br />
|
||||
<span className="text-danger fw-semibold">
|
||||
Para asegurar el funcionamiento óptimo del prellenado, modifica la
|
||||
estructura solo si es necesario.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="d-flex gap-4 flex-wrap">
|
||||
{plantillasDisponibles.map((plantilla) => (
|
||||
<div
|
||||
className="mb-3 cursor-pointer"
|
||||
key={plantilla.id}
|
||||
onClick={() => handleSeleccionarPlantilla(plantilla.id)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Image
|
||||
src={plantilla.imagen}
|
||||
width={200}
|
||||
height={200}
|
||||
alt={`Plantilla de formulario ${plantilla.nombre}`}
|
||||
className="img-fluid rounded shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{datosFormulario && (
|
||||
<CreateFormulario
|
||||
evento={evento ? evento : undefined}
|
||||
formulario={datosFormulario}
|
||||
onChange={(nuevo) => setDatosFormulario({ ...nuevo })}
|
||||
/>
|
||||
)}
|
||||
<Button className="mb-4" onClick={handleCrearEvento}>
|
||||
Crear Evento y Formulario
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
import { EventoProvider } from '@/context/evento/evento-context';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const params = useParams<Params>();
|
||||
|
||||
if (!params.id_evento) {
|
||||
return <div>Error: ID de evento no encontrado</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventoProvider id_evento={params.id_evento}>{children}</EventoProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +1,51 @@
|
||||
'use client';
|
||||
import EditEvento from '@/containers/edit-evento';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import {
|
||||
CuestionarioWithCupo,
|
||||
GetEventoWithCuestionariosWithCupos,
|
||||
} from '@/types/evento';
|
||||
import { useParams } from 'next/navigation';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import React, { useState } from 'react';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import { useEvento } from '@/context/evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import Table, { Header } from '@/components/table';
|
||||
import Button from '@/components/button';
|
||||
import { downloadFile } from '@/utils/downloas-utils';
|
||||
import Link from 'next/link';
|
||||
import NewCuestionarioCard from '@/components/new-cuestionario-card';
|
||||
import Breadcrumb from '@/components/breadcrumb';
|
||||
import Button from '@/components/button';
|
||||
|
||||
type Params = {
|
||||
id_evento: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const params = useParams<Params>();
|
||||
const { data } = useGetApi<GetEventoWithCuestionariosWithCupos>(
|
||||
`/evento/${params.id_evento}/cuestionarios`
|
||||
const { evento, loading, error, updateEvento, setEventoData } = useEvento();
|
||||
|
||||
const [loadingStates, setLoadingStates] = useState<Record<number, boolean>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const [evento, setEvento] =
|
||||
useState<GetEventoWithCuestionariosWithCupos | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setEvento(data);
|
||||
}, [data]);
|
||||
|
||||
const handleChange = (
|
||||
field: keyof CreateEventoType,
|
||||
value: string | Date
|
||||
) => {
|
||||
setEvento((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
[field]: value,
|
||||
}
|
||||
: null
|
||||
);
|
||||
// Update the evento state directly using the context
|
||||
updateEvento({ [field]: value });
|
||||
};
|
||||
|
||||
const handleEventoActualizado = (
|
||||
eventoActualizado: GetEventoWithCuestionariosWithCupos
|
||||
) => {
|
||||
setEvento(eventoActualizado);
|
||||
// Set the complete updated evento data
|
||||
setEventoData(eventoActualizado);
|
||||
};
|
||||
|
||||
const headers: Header<CuestionarioWithCupo>[] = [
|
||||
{
|
||||
key: 'nombre_form',
|
||||
label: 'Nombre del cuestionario',
|
||||
},
|
||||
{
|
||||
key: 'cupo_maximo',
|
||||
label: 'Cupos',
|
||||
render: (_, row) => {
|
||||
if (row.cupo_maximo === null) {
|
||||
return 'Sin límite';
|
||||
}
|
||||
|
||||
return `${row.cupos_usados ?? 0} / ${row.cupo_maximo}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'banner',
|
||||
label: 'Ver / Editar',
|
||||
render: (_, row) => (
|
||||
<div className="d-flex gap-2">
|
||||
<Link
|
||||
className="btn btn-primary"
|
||||
href={`/administrador/evento/${params.id_evento}/${row.id_cuestionario}`}
|
||||
>
|
||||
Ver/Editar formulario
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'banner',
|
||||
label: 'Descargar respuestas',
|
||||
render: (_, row) => (
|
||||
<Button
|
||||
variant="success"
|
||||
icon="download"
|
||||
onClick={() => handleDownload(row.id_cuestionario, row.nombre_form)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Descargando...
|
||||
</>
|
||||
) : (
|
||||
'Descargar respuestas'
|
||||
)}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (!evento) return <p>Cargando...</p>;
|
||||
if (loading) return <p>Cargando...</p>;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
if (!evento) return <p>No se encontró el evento</p>;
|
||||
|
||||
const handleDownload = async (id_cuestionario: number, nombre: string) => {
|
||||
setLoading(true);
|
||||
setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: true }));
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
@@ -128,18 +60,21 @@ export default function Page() {
|
||||
console.error('Error al descargar el archivo:', error);
|
||||
toast.error('Error al descargar el archivo');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Link href={`/administrador`} className="btn btn-link mb-3">
|
||||
<i className="bi bi-arrow-left me-2"></i>
|
||||
Volver
|
||||
</Link>
|
||||
|
||||
<h2 className="mb-4">Evento</h2>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Eventos', href: '/administrador/eventos' },
|
||||
{
|
||||
label: `${evento.nombre_evento}`,
|
||||
href: `/administrador/evento/${params.id_evento}`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<EditEvento
|
||||
evento={evento}
|
||||
@@ -147,13 +82,48 @@ export default function Page() {
|
||||
handleOnChange={handleEventoActualizado}
|
||||
/>
|
||||
|
||||
{data?.cuestionarios && data.cuestionarios.length > 0 && (
|
||||
<>
|
||||
<h3 className="mt-5">Cuestionarios del evento</h3>
|
||||
<div className="mt-2">
|
||||
<Table headers={headers} data={data.cuestionarios ?? []} />
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 className="mb-0">Formularios del evento</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Aquí puedes ver y gestionar los formularios asociados a este
|
||||
evento.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/administrador/evento/${params.id_evento}/formularios/crear`
|
||||
)
|
||||
}
|
||||
>
|
||||
Crear nuevo formulario
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{evento?.cuestionarios && evento.cuestionarios.length > 0 && (
|
||||
<div className="row mt-3">
|
||||
{evento.cuestionarios.map((item, key) => {
|
||||
const fadeClass = `delay-${(key % 5) + 1}`;
|
||||
return (
|
||||
<div
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={key}
|
||||
>
|
||||
<NewCuestionarioCard
|
||||
cuestionario={item}
|
||||
user="administrador"
|
||||
onDownload={handleDownload}
|
||||
loading={loadingStates[item.id_cuestionario] || false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import CreateEvento from '@/containers/create-evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getAxiosError } from '@/utils/errors-utils';
|
||||
import Button from '@/components/button';
|
||||
|
||||
export default function Page() {
|
||||
const [eventoBanner, setEventoBanner] = useState<File | null>(null);
|
||||
const [evento, setEvento] = useState<CreateEventoType>({
|
||||
tipo_evento: '',
|
||||
nombre_evento: '',
|
||||
descripcion_evento: '',
|
||||
fecha_inicio: new Date(),
|
||||
fecha_fin: new Date(),
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
const handleChange = (
|
||||
field: keyof CreateEventoType,
|
||||
value: string | Date
|
||||
) => {
|
||||
setEvento((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBannerChange = (file: File | null) => {
|
||||
setEventoBanner(file);
|
||||
};
|
||||
|
||||
const handleCrearEvento = async () => {
|
||||
try {
|
||||
const createEvento = await axiosInstance.post('/evento', evento);
|
||||
|
||||
if (createEvento) {
|
||||
const formData = new FormData();
|
||||
if (eventoBanner) {
|
||||
formData.append('banner', eventoBanner);
|
||||
const bannercreated = await axiosInstance.post(
|
||||
`/evento/${createEvento.data.id_evento}/banner`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log('Banner creado:', bannercreated.data);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('Evento y formulario creados exitosamente');
|
||||
router.push(
|
||||
`/administrador/evento/${createEvento.data.id_evento}/formularios/crear`
|
||||
);
|
||||
} catch (error) {
|
||||
const msg = getAxiosError(error);
|
||||
toast.error(
|
||||
`Error al crear el evento o formulario: ${
|
||||
msg.message || 'Error desconocido'
|
||||
}`
|
||||
);
|
||||
console.error('Error al crear evento y formulario:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<CreateEvento
|
||||
evento={evento}
|
||||
handleChange={handleChange}
|
||||
handleBannerChange={handleBannerChange}
|
||||
/>
|
||||
|
||||
{/* Botón de guardar en la parte inferior */}
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button icon="arrow-right" onClick={handleCrearEvento}>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+6
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
import Button from '@/components/button';
|
||||
import CuestionarioCard from '@/components/cuestionario-card';
|
||||
import EventoCardAdmin from '@/components/evento/evento-card-admin';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import Link from 'next/link';
|
||||
@@ -14,7 +15,6 @@ export default function Page() {
|
||||
const { data: recientes } = useGetApi<GetEventoWithCuestionariosWithCupos[]>(
|
||||
'/evento/recientes/cuestionarios'
|
||||
);
|
||||
const { data: eventos } = useGetApi('/evento/activos');
|
||||
|
||||
if (loading) return <div>Cargando formularios...</div>;
|
||||
if (error) return <div className="alert alert-danger">{error.message}</div>;
|
||||
@@ -23,13 +23,14 @@ export default function Page() {
|
||||
<>
|
||||
<div className="d-flex justify-content-between align-items-center my-4">
|
||||
<h1>Eventos</h1>
|
||||
<Link href="/administrador/crear-evento">
|
||||
<Link href="/administrador/eventos/crear">
|
||||
<Button>Crear Evento</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{loading && <div>Cargando...</div>}
|
||||
{data && (
|
||||
{data && data.length > 0 && (
|
||||
<div className="row">
|
||||
<h1>Eventos Activos</h1>
|
||||
{data.map((item, key) => {
|
||||
const fadeClass = `delay-${(key % 5) + 1}`;
|
||||
return (
|
||||
@@ -37,13 +38,13 @@ export default function Page() {
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={key}
|
||||
>
|
||||
<CuestionarioCard evento={item} user="administrador" />
|
||||
<EventoCardAdmin evento={item} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{recientes && (
|
||||
{recientes && recientes.length > 0 && (
|
||||
<div className="row">
|
||||
<h2>Eventos Recientes</h2>
|
||||
<p className="text-muted">Eventos de los ultimos 30 días.</p>
|
||||
@@ -60,7 +61,6 @@ export default function Page() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<pre>{JSON.stringify(eventos, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export default function Page() {
|
||||
|
||||
if (username === 'user-admin' && password === '@dm1nP@ss') {
|
||||
Cookies.set('token', 'staff1');
|
||||
router.push('/administrador');
|
||||
router.push('/administrador/eventos');
|
||||
} else {
|
||||
setError('Usuario o contraseña incorrectos');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// src/app/(public)/evento/[evento]/[cuestionario]/page.tsx
|
||||
import FormularioPage from '@/containers/pages/formulario-page';
|
||||
import { GetEventoWithCuestionario } from '@/types/evento';
|
||||
import { fromParam } from '@/utils/slugify';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
type PageParams = {
|
||||
evento: string; // "carrera-de-botargas-123"
|
||||
cuestionario: string; // "formulario-inscripcion-456"
|
||||
};
|
||||
|
||||
type Props = {
|
||||
params: Promise<PageParams>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { evento, cuestionario } = await params; // <- await
|
||||
const { id: id_evento } = fromParam(evento);
|
||||
const { id: id_cuestionario } = fromParam(cuestionario);
|
||||
|
||||
if (!id_evento || !id_cuestionario) {
|
||||
return { title: 'Evento', description: 'Descripción del evento' };
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/evento/${id_evento}/cuestionario/${id_cuestionario}`,
|
||||
{ next: { revalidate: 60 } }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return { title: 'Evento', description: 'Descripción del evento' };
|
||||
}
|
||||
|
||||
const data: GetEventoWithCuestionario = await res.json();
|
||||
const title = data?.nombre_evento || 'Evento';
|
||||
const description = data?.descripcion_evento || 'Descripción del evento';
|
||||
const banner = data?.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${data.banner}`
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
images: banner
|
||||
? [{ url: banner, width: 800, height: 400, alt: title }]
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({ params }: Props) {
|
||||
const { evento, cuestionario } = await params; // <- await
|
||||
const { id: id_evento } = fromParam(evento);
|
||||
const { id: id_cuestionario } = fromParam(cuestionario);
|
||||
|
||||
if (!id_evento || !id_cuestionario) {
|
||||
// import { notFound } from 'next/navigation';
|
||||
// return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormularioPage
|
||||
id_evento={String(id_evento)}
|
||||
id_cuestionario={String(id_cuestionario)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import FormularioPage from '@/containers/pages/formulario-page';
|
||||
import { GetEventoWithCuestionario } from '@/types/evento';
|
||||
import { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
|
||||
type Params = {
|
||||
nombre_evento: string;
|
||||
id_evento: string;
|
||||
id_cuestionario: string;
|
||||
};
|
||||
|
||||
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}`
|
||||
);
|
||||
|
||||
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 (
|
||||
<FormularioPage id_evento={id_evento} id_cuestionario={id_cuestionario} />
|
||||
);
|
||||
}
|
||||
+43
-19
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import ClientCarousel from '@/client-components/client-carousel';
|
||||
import CuestionarioCard from '@/components/cuestionario-card';
|
||||
import FormularioCardUser from '@/components/formulario/formulario-card-user';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import { GetEvento, GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import React from 'react';
|
||||
|
||||
export default function Page() {
|
||||
@@ -15,28 +15,52 @@ export default function Page() {
|
||||
<div>
|
||||
<div className="mx-auto mb-5 mt-3 fade-in-down-bounce">
|
||||
<ClientCarousel
|
||||
images={[
|
||||
{
|
||||
src: '/banner.jpeg',
|
||||
alt: 'Banner de eventos activos',
|
||||
},
|
||||
]}
|
||||
images={
|
||||
data
|
||||
? data.map((evento) => ({
|
||||
src: `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`,
|
||||
alt: `Banner de ${evento.nombre_evento}`,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
src: '/banner.jpeg',
|
||||
alt: 'Banner de eventos activos',
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{loading && <div>Cargando...</div>}
|
||||
{data && (
|
||||
<div className="row">
|
||||
{data.map((item, key) => {
|
||||
const fadeClass = `delay-${(key % 5) + 1}`;
|
||||
return (
|
||||
<div
|
||||
className={`col-md-6 col-lg-4 my-4 fade-in-up-bounce ${fadeClass}`}
|
||||
key={key}
|
||||
>
|
||||
<CuestionarioCard evento={item} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{data.flatMap((evento) =>
|
||||
evento.cuestionarios.map((cuestionario, index) => {
|
||||
const eventoData: GetEvento = {
|
||||
id_evento: evento.id_evento,
|
||||
nombre_evento: evento.nombre_evento,
|
||||
descripcion_evento: evento.descripcion_evento,
|
||||
tipo_evento: evento.tipo_evento,
|
||||
fecha_inicio: evento.fecha_inicio,
|
||||
fecha_fin: evento.fecha_fin,
|
||||
asistencias: evento.asistencias,
|
||||
banner: evento.banner,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`col-md-6 col-lg-4 my-3 fade-in-up-bounce delay-${
|
||||
(index % 5) + 1
|
||||
}`}
|
||||
key={`${evento.id_evento}-${cuestionario.id_cuestionario}`}
|
||||
>
|
||||
<FormularioCardUser
|
||||
evento={eventoData}
|
||||
formulario={cuestionario}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,14 @@ interface BannerUploaderProps {
|
||||
label?: string;
|
||||
onChange?: (file: File | null) => void;
|
||||
defaultBanner?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ImageUploader({
|
||||
label = 'Subir imagen',
|
||||
onChange,
|
||||
defaultBanner,
|
||||
className = '',
|
||||
}: BannerUploaderProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [preview, setPreview] = useState<string | null>(defaultBanner || null);
|
||||
@@ -41,17 +43,28 @@ export default function ImageUploader({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className={className}>
|
||||
{label && <label className="block mb-2 font-semibold">{label}</label>}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="form-control mb-2"
|
||||
/>
|
||||
<div className="d-flex gap-2 align-items-start mb-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="form-control"
|
||||
/>
|
||||
{preview && (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleRemoveImage}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<i className="bi bi-trash"></i>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{preview && (
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<div className="d-flex justify-content-center">
|
||||
<Image
|
||||
src={preview}
|
||||
alt="Previsualización"
|
||||
@@ -59,13 +72,6 @@ export default function ImageUploader({
|
||||
width={600}
|
||||
height={300}
|
||||
/>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleRemoveImage}
|
||||
className="mt-3"
|
||||
>
|
||||
Eliminar imagen
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export default function Breadcrumb({ items }: BreadcrumbProps) {
|
||||
return (
|
||||
<nav aria-label="breadcrumb" className="mb-3">
|
||||
<ol className="breadcrumb">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className={`breadcrumb-item ${isLast ? 'active' : ''}`}
|
||||
aria-current={isLast ? 'page' : undefined}
|
||||
>
|
||||
{isLast ? (
|
||||
item.label
|
||||
) : (
|
||||
<Link href={item.href || '#'} className="text-decoration-none">
|
||||
{item.label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from './markdown-render';
|
||||
|
||||
export default function EventoCard({
|
||||
export default function CuestionarioCard({
|
||||
evento,
|
||||
user = 'public',
|
||||
}: {
|
||||
|
||||
@@ -1,5 +1,166 @@
|
||||
import React from 'react';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from './markdown-render';
|
||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||
|
||||
export default function EventoCard() {
|
||||
return <div>EventoCard</div>;
|
||||
export default function EventoCard({
|
||||
evento,
|
||||
user = 'public',
|
||||
}: {
|
||||
evento: GetEventoWithCuestionariosWithCupos;
|
||||
user?: 'public' | 'administrador' | 'staff';
|
||||
}) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = evento.descripcion_evento ?? '';
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
return (
|
||||
<div className="card card-blog d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div className="card-image scale-hover-1">
|
||||
<Link
|
||||
href={
|
||||
user === 'administrador' || user === 'staff'
|
||||
? `/${user}/evento/${evento.id_evento}`
|
||||
: `/evento/${evento.nombre_evento.split(' ').join('_')}/${
|
||||
evento.id_evento
|
||||
}/${evento.cuestionarios[0].id_cuestionario}`
|
||||
}
|
||||
>
|
||||
<Image
|
||||
width={400}
|
||||
height={300}
|
||||
className="img-fluid"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`}
|
||||
alt="Banner formulario"
|
||||
/>
|
||||
<div className="card-caption">{evento.nombre_evento}</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-description mb-0">
|
||||
<MarkdownRenderer markdown={descripcionRecortada} />
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{evento.cuestionarios.length > 0 ? (
|
||||
<>
|
||||
<h2 className="my-2 fs-3">Formularios</h2>
|
||||
<table className="table table-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Nombre</th>
|
||||
<th scope="col">Inscritos</th>
|
||||
<th scope="col">Capacidad total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{evento.cuestionarios.map((cuestionario) => (
|
||||
<tr key={cuestionario.id_cuestionario}>
|
||||
<td>
|
||||
<Link
|
||||
href={
|
||||
user === 'administrador' || user === 'staff'
|
||||
? `/${user}/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`
|
||||
: `/evento/${evento.nombre_evento
|
||||
.split(' ')
|
||||
.join('_')}/${evento.id_evento}/${
|
||||
cuestionario.id_cuestionario
|
||||
}`
|
||||
}
|
||||
>
|
||||
{cuestionario.nombre_form}
|
||||
</Link>
|
||||
</td>
|
||||
{cuestionario.cupo_maximo ? (
|
||||
<>
|
||||
<td>{cuestionario.cupos_usados}</td>
|
||||
<td>{cuestionario.cupo_maximo}</td>
|
||||
</>
|
||||
) : (
|
||||
<td colSpan={2} className="text-center">
|
||||
Sin límite
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center px-4 py-3 border rounded bg-light my-3">
|
||||
<i className="bi bi-clipboard-x fs-1 text-muted mb-3"></i>
|
||||
<p className="text-muted mb-3">
|
||||
Este evento no tiene formularios disponibles.
|
||||
</p>
|
||||
{(user === 'administrador' || user === 'staff') && (
|
||||
<Link
|
||||
href={`/${user}/evento/${evento.id_evento}/formularios/crear`}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<i className="bi bi-plus-circle me-2"></i>
|
||||
Crear formulario
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="badge fs-6 w-100 bg-primary">
|
||||
Evento {formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)}
|
||||
</span>
|
||||
|
||||
{/* Botón único del evento */}
|
||||
<div className="mt-2">
|
||||
{user === 'administrador' || user === 'staff' ? (
|
||||
<Link
|
||||
href={`/${user}/evento/${evento.id_evento}`}
|
||||
className="btn w-100 btn-outline-primary"
|
||||
>
|
||||
Ver/Editar evento
|
||||
</Link>
|
||||
) : evento.cuestionarios.length === 0 ? (
|
||||
<button className="btn w-100 btn-outline-secondary" disabled>
|
||||
Sin formularios disponibles
|
||||
</button>
|
||||
) : evento.cuestionarios.some(
|
||||
(cuestionario) =>
|
||||
cuestionario.cupos_disponibles === 0 &&
|
||||
cuestionario.cupo_maximo !== null
|
||||
) &&
|
||||
evento.cuestionarios.every(
|
||||
(cuestionario) =>
|
||||
cuestionario.cupos_disponibles === 0 &&
|
||||
cuestionario.cupo_maximo !== null
|
||||
) ? (
|
||||
<button className="btn w-100 btn-outline-secondary" disabled>
|
||||
Cupo lleno
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/evento/${evento.nombre_evento.split(' ').join('_')}/${
|
||||
evento.id_evento
|
||||
}/${evento.cuestionarios[0]?.id_cuestionario || ''}`}
|
||||
className="btn w-100 btn-outline-primary"
|
||||
>
|
||||
Registro
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from '../markdown-render';
|
||||
|
||||
export default function EventoCardAdmin({
|
||||
evento,
|
||||
}: {
|
||||
evento: GetEventoWithCuestionariosWithCupos;
|
||||
}) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = evento.descripcion_evento ?? '';
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card shadow-sm border-0"
|
||||
style={{ borderRadius: '12px', overflow: 'hidden' }}
|
||||
>
|
||||
<div className="position-relative">
|
||||
<Link href={`/administrador/evento/${evento.id_evento}`}>
|
||||
<Image
|
||||
width={400}
|
||||
height={250}
|
||||
className="img-fluid w-100"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`}
|
||||
alt="Banner formulario"
|
||||
style={{ objectFit: 'cover', height: '200px' }}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Badge de cantidad de formularios en la esquina superior derecha */}
|
||||
<div className="position-absolute top-0 end-0 m-2">
|
||||
{evento.cuestionarios.length > 0 ? (
|
||||
<span className="badge bg-primary">
|
||||
{evento.cuestionarios.length} formulario
|
||||
{evento.cuestionarios.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge bg-secondary">Sin formularios</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-body p-3 d-flex flex-column">
|
||||
{/* Fecha del evento */}
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<div className="me-3">
|
||||
<div className="text-muted text-center small">
|
||||
{new Date(evento.fecha_inicio)
|
||||
.toLocaleDateString('es-ES', { month: 'short' })
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<div
|
||||
className="fw-bold h4 mb-0"
|
||||
style={{ lineHeight: 1, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{(() => {
|
||||
const fechaInicio = new Date(evento.fecha_inicio);
|
||||
const fechaFin = new Date(evento.fecha_fin);
|
||||
const diaInicio = fechaInicio.getDate();
|
||||
const diaFin = fechaFin.getDate();
|
||||
const esElMismoDia =
|
||||
fechaInicio.toDateString() === fechaFin.toDateString();
|
||||
|
||||
return esElMismoDia ? diaInicio : `${diaInicio} - ${diaFin}`;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="card-title mb-1 fw-bold">{evento.nombre_evento}</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Descripción */}
|
||||
<div className="card-description flex-grow-1 mb-3">
|
||||
<MarkdownRenderer markdown={descripcionRecortada} />
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline text-decoration-none"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Formularios/sub eventos */}
|
||||
{evento.cuestionarios.length > 0 ? (
|
||||
<div className="mb-3">
|
||||
<h6 className="fw-bold mb-2 text-muted">
|
||||
Formularios disponibles:
|
||||
</h6>
|
||||
<div className="row row-cols-1 g-2">
|
||||
{evento.cuestionarios.map((cuestionario) => (
|
||||
<div key={cuestionario.id_cuestionario} className="col">
|
||||
<div className="py-2 px-3 bg-light border-0">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
className="text-decoration-none fw-semibold"
|
||||
>
|
||||
{cuestionario.nombre_form}
|
||||
</Link>
|
||||
<small className="text-muted">
|
||||
{cuestionario.cupo_maximo
|
||||
? `${cuestionario.cupos_usados}/${cuestionario.cupo_maximo}`
|
||||
: 'Sin límite'}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center px-3 py-3 bg-light rounded mb-3">
|
||||
<i className="bi bi-clipboard-x fs-4 text-muted mb-2 d-block"></i>
|
||||
<p className="text-muted mb-2 small">Sin formularios disponibles</p>
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}/formularios/crear`}
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
<i className="bi bi-plus-circle me-1"></i>
|
||||
Crear formulario
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Botón principal */}
|
||||
<Link
|
||||
href={`/administrador/evento/${evento.id_evento}`}
|
||||
className="btn btn-primary w-100 rounded-pill"
|
||||
>
|
||||
Ver/Editar evento
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from '../markdown-render';
|
||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||
|
||||
export default function EventoCard({
|
||||
evento,
|
||||
user = 'public',
|
||||
}: {
|
||||
evento: GetEventoWithCuestionariosWithCupos;
|
||||
user?: 'public' | 'administrador' | 'staff';
|
||||
}) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = evento.descripcion_evento ?? '';
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
return (
|
||||
<div className="card card-blog d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div className="card-image scale-hover-1">
|
||||
<Link
|
||||
href={
|
||||
user === 'administrador' || user === 'staff'
|
||||
? `/${user}/evento/${evento.id_evento}`
|
||||
: `/evento/${evento.nombre_evento.split(' ').join('_')}/${
|
||||
evento.id_evento
|
||||
}/${evento.cuestionarios[0].id_cuestionario}`
|
||||
}
|
||||
>
|
||||
<Image
|
||||
width={400}
|
||||
height={300}
|
||||
className="img-fluid"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`}
|
||||
alt="Banner formulario"
|
||||
/>
|
||||
<div className="card-caption">{evento.nombre_evento}</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-description mb-0">
|
||||
<MarkdownRenderer markdown={descripcionRecortada} />
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{evento.cuestionarios.length > 0 ? (
|
||||
<>
|
||||
<h2 className="my-2 fs-3">Formularios</h2>
|
||||
<table className="table table-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Nombre</th>
|
||||
<th scope="col">Inscritos</th>
|
||||
<th scope="col">Capacidad total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{evento.cuestionarios.map((cuestionario) => (
|
||||
<tr key={cuestionario.id_cuestionario}>
|
||||
<td>
|
||||
<Link
|
||||
href={
|
||||
user === 'administrador' || user === 'staff'
|
||||
? `/${user}/evento/${evento.id_evento}/formulario/${cuestionario.id_cuestionario}`
|
||||
: `/evento/${evento.nombre_evento
|
||||
.split(' ')
|
||||
.join('_')}/${evento.id_evento}/${
|
||||
cuestionario.id_cuestionario
|
||||
}`
|
||||
}
|
||||
>
|
||||
{cuestionario.nombre_form}
|
||||
</Link>
|
||||
</td>
|
||||
{cuestionario.cupo_maximo ? (
|
||||
<>
|
||||
<td>{cuestionario.cupos_usados}</td>
|
||||
<td>{cuestionario.cupo_maximo}</td>
|
||||
</>
|
||||
) : (
|
||||
<td colSpan={2} className="text-center">
|
||||
Sin límite
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center px-4 py-3 border rounded bg-light my-3">
|
||||
<i className="bi bi-clipboard-x fs-1 text-muted mb-3"></i>
|
||||
<p className="text-muted mb-3">
|
||||
Este evento no tiene formularios disponibles.
|
||||
</p>
|
||||
{(user === 'administrador' || user === 'staff') && (
|
||||
<Link
|
||||
href={`/${user}/evento/${evento.id_evento}/formularios/crear`}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<i className="bi bi-plus-circle me-2"></i>
|
||||
Crear formulario
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className="badge fs-6 w-100 bg-primary">
|
||||
Evento {formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)}
|
||||
</span>
|
||||
|
||||
{/* Botón único del evento */}
|
||||
<div className="mt-2">
|
||||
{user === 'administrador' || user === 'staff' ? (
|
||||
<Link
|
||||
href={`/${user}/evento/${evento.id_evento}`}
|
||||
className="btn w-100 btn-outline-primary"
|
||||
>
|
||||
Ver/Editar evento
|
||||
</Link>
|
||||
) : evento.cuestionarios.length === 0 ? (
|
||||
<button className="btn w-100 btn-outline-secondary" disabled>
|
||||
Sin formularios disponibles
|
||||
</button>
|
||||
) : evento.cuestionarios.some(
|
||||
(cuestionario) =>
|
||||
cuestionario.cupos_disponibles === 0 &&
|
||||
cuestionario.cupo_maximo !== null
|
||||
) &&
|
||||
evento.cuestionarios.every(
|
||||
(cuestionario) =>
|
||||
cuestionario.cupos_disponibles === 0 &&
|
||||
cuestionario.cupo_maximo !== null
|
||||
) ? (
|
||||
<button className="btn w-100 btn-outline-secondary" disabled>
|
||||
Cupo lleno
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/evento/${evento.nombre_evento.split(' ').join('_')}/${
|
||||
evento.id_evento
|
||||
}/${evento.cuestionarios[0]?.id_cuestionario || ''}`}
|
||||
className="btn w-100 btn-outline-primary"
|
||||
>
|
||||
Registro
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function FormularioCardAdmin() {
|
||||
return <div>FormularioCardAdmin</div>;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
import { CuestionarioWithCupo, GetEvento } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from '../markdown-render';
|
||||
import { toParam } from '@/utils/slugify';
|
||||
|
||||
export default function FormularioCardUser({
|
||||
evento,
|
||||
formulario,
|
||||
}: {
|
||||
evento: GetEvento;
|
||||
formulario: CuestionarioWithCupo;
|
||||
}) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = formulario.descripcion ?? '';
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="card shadow-sm border-0"
|
||||
style={{ borderRadius: '12px', overflow: 'hidden' }}
|
||||
>
|
||||
<div className="position-relative">
|
||||
<Link
|
||||
href={`/evento/${toParam(
|
||||
evento.nombre_evento,
|
||||
evento.id_evento
|
||||
)}/${toParam(formulario.nombre_form, formulario.id_cuestionario)}`}
|
||||
>
|
||||
<Image
|
||||
width={400}
|
||||
height={250}
|
||||
className="img-fluid w-100"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}`}
|
||||
alt="Banner formulario"
|
||||
style={{ objectFit: 'cover', height: '200px' }}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Badge de cupos en la esquina superior derecha */}
|
||||
<div className="position-absolute top-0 end-0 m-2">
|
||||
{formulario.cupo_maximo === null ? (
|
||||
<span className="badge bg-success">Sin límite</span>
|
||||
) : (
|
||||
<span className="badge bg-primary">
|
||||
{formulario.cupos_disponibles} cupos
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-body p-3 d-flex flex-column">
|
||||
{/* Fecha del evento */}
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<div className="me-3">
|
||||
<div className="text-muted text-center small">
|
||||
{new Date(formulario.fecha_inicio)
|
||||
.toLocaleDateString('es-ES', { month: 'short' })
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<div
|
||||
className="fw-bold h4 mb-0"
|
||||
style={{ lineHeight: 1, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{(() => {
|
||||
const fechaInicio = new Date(formulario.fecha_inicio);
|
||||
const fechaFin = new Date(formulario.fecha_fin);
|
||||
const diaInicio = fechaInicio.getDate();
|
||||
const diaFin = fechaFin.getDate();
|
||||
const esElMismoDia =
|
||||
fechaInicio.toDateString() === fechaFin.toDateString();
|
||||
|
||||
return esElMismoDia ? diaInicio : `${diaInicio} - ${diaFin}`;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="small text-muted fw-bold">
|
||||
{evento.nombre_evento}
|
||||
</span>
|
||||
<h5 className="card-title mb-1 fw-bold">
|
||||
{formulario.nombre_form}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Descripción */}
|
||||
<div className="card-description flex-grow-1 mb-3">
|
||||
<MarkdownRenderer markdown={descripcionRecortada} />
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline text-decoration-none"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Botón de registro */}
|
||||
<Link
|
||||
href={`/evento/${toParam(
|
||||
evento.nombre_evento,
|
||||
evento.id_evento
|
||||
)}/${toParam(formulario.nombre_form, formulario.id_cuestionario)}`}
|
||||
className="btn btn-primary w-100 rounded-pill"
|
||||
>
|
||||
Registro
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
import { CuestionarioWithCupo } from '@/types/evento';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import React, { useState } from 'react';
|
||||
import MarkdownRenderer from './markdown-render';
|
||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||
|
||||
export default function NewCuestionarioCard({
|
||||
cuestionario,
|
||||
user = 'public',
|
||||
onDownload,
|
||||
loading = false,
|
||||
}: {
|
||||
cuestionario: CuestionarioWithCupo;
|
||||
user?: 'public' | 'administrador' | 'staff';
|
||||
onDownload?: (id_cuestionario: number, nombre: string) => void;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const [verMas, setVerMas] = useState(false);
|
||||
const descripcion = cuestionario.descripcion ?? '';
|
||||
const limite = 100;
|
||||
|
||||
const descripcionRecortada =
|
||||
descripcion.length > limite && !verMas
|
||||
? `${descripcion.slice(0, limite)}...`
|
||||
: descripcion;
|
||||
|
||||
return (
|
||||
<div className="card card-blog d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<div className="card-image scale-hover-1">
|
||||
<Link
|
||||
href={
|
||||
user === 'administrador' || user === 'staff'
|
||||
? `/${user}/evento/${cuestionario.id_evento}`
|
||||
: `/evento/${cuestionario.nombre_form.split(' ').join('_')}/${
|
||||
cuestionario.id_evento
|
||||
}/${cuestionario.id_cuestionario}`
|
||||
}
|
||||
>
|
||||
<Image
|
||||
width={400}
|
||||
height={300}
|
||||
className="img-fluid"
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`}
|
||||
alt="Banner formulario"
|
||||
/>
|
||||
<div className="card-caption">{cuestionario.nombre_form}</div>
|
||||
</Link>
|
||||
<div className="ripple-cont"></div>
|
||||
</div>
|
||||
|
||||
<div className="card-body">
|
||||
<div className="card-description mb-0">
|
||||
{cuestionario.cupo_maximo === null ? (
|
||||
<span className="badge bg-success mb-2">Sin límite</span>
|
||||
) : (
|
||||
<span className="badge bg-primary mb-2">
|
||||
{cuestionario.cupos_disponibles} cupos disponibles
|
||||
</span>
|
||||
)}
|
||||
<MarkdownRenderer markdown={descripcionRecortada} />
|
||||
{descripcion.length > limite && (
|
||||
<button
|
||||
onClick={() => setVerMas(!verMas)}
|
||||
className="btn btn-link btn-sm p-0 ms-1 align-baseline"
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{verMas ? 'Ver menos' : 'Ver más'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className="badge fs-6 w-100 bg-primary">
|
||||
Evento{' '}
|
||||
{formatearRangoFechas(
|
||||
cuestionario.fecha_inicio,
|
||||
cuestionario.fecha_fin
|
||||
)}
|
||||
</span>
|
||||
<div className="">
|
||||
{user === 'administrador' || user === 'staff' ? (
|
||||
<>
|
||||
<Link
|
||||
href={`/${user}/evento/${cuestionario.id_evento}/formulario/${cuestionario.id_cuestionario}`}
|
||||
className="btn mt-2 w-100 btn-outline-primary"
|
||||
>
|
||||
Ver formulario
|
||||
</Link>
|
||||
{user === 'administrador' && onDownload && (
|
||||
<button
|
||||
className="btn mt-2 w-100 btn-success"
|
||||
onClick={() =>
|
||||
onDownload(
|
||||
cuestionario.id_cuestionario,
|
||||
cuestionario.nombre_form
|
||||
)
|
||||
}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span
|
||||
className="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
Descargando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi bi-download me-2"></i>
|
||||
Descargar respuestas
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : cuestionario.cupos_disponibles === 0 &&
|
||||
cuestionario.cupo_maximo !== null ? (
|
||||
<button className="btn mt-2 w-100 btn-outline-secondary" disabled>
|
||||
Cupo lleno
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/evento/${cuestionario.nombre_form
|
||||
.split(' ')
|
||||
.join('_')}/${cuestionario.id_evento}/${
|
||||
cuestionario.id_cuestionario
|
||||
}`}
|
||||
className="btn mt-2 w-100 btn-outline-primary"
|
||||
>
|
||||
Registro
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import ImageUploader from '@/components/banner-uploader';
|
||||
import Input from '@/components/input';
|
||||
import Select from '@/components/select';
|
||||
import { CreateEventoType } from '@/types/create-evento';
|
||||
import { tipoEventos } from '@/utils/arrays';
|
||||
import { formatDateLocal } from '@/utils/date-utils';
|
||||
import { commands } from '@uiw/react-md-editor';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -24,80 +25,109 @@ export default function CreateEvento({
|
||||
handleBannerChange,
|
||||
}: CreateEventoProps) {
|
||||
return (
|
||||
<div className="row border p-3 rounded my-4">
|
||||
<h2>Información del Evento</h2>
|
||||
<p>
|
||||
Completa la siguiente información para describir los detalles generales
|
||||
del evento.
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Creación del Evento</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Completa la siguiente información para describir los detalles
|
||||
generales del evento.
|
||||
</p>
|
||||
<p className="mb-0 text-muted">
|
||||
Presiona siguiente para continuar con la creacion del formulario.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ImageUploader label="Banner del evento" onChange={handleBannerChange} />
|
||||
|
||||
<Input
|
||||
label="Nombre del evento"
|
||||
required
|
||||
value={evento.nombre_evento}
|
||||
onChange={(e) => handleChange('nombre_evento', e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Descripción del evento</label>
|
||||
<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 className="row mt-3">
|
||||
{/* Columna Izquierda - Banner */}
|
||||
<div className="col-lg-6">
|
||||
<div className="pe-lg-3">
|
||||
<ImageUploader
|
||||
label="Banner del evento"
|
||||
onChange={handleBannerChange}
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
Esta es la imagen que se verá en el carrusel del evento.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-image me-2"></i>
|
||||
Dimensiones recomendadas: 1200x600 píxeles.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-file-earmark-image me-2"></i>
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Tipo de evento"
|
||||
value={evento.tipo_evento}
|
||||
placeholder="Selecciona un tipo de evento"
|
||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||
options={[
|
||||
{ label: 'Conferencia', value: 'conferencia' },
|
||||
{ label: 'Taller', value: 'taller' },
|
||||
{ label: 'Seminario', value: 'seminario' },
|
||||
{ label: 'Curso', value: 'curso' },
|
||||
{ label: 'Webinar', value: 'webinar' },
|
||||
{ label: 'Reunión', value: 'reunion' },
|
||||
{ label: 'Feria', value: 'feria' },
|
||||
{ label: 'Concierto', value: 'concierto' },
|
||||
{ label: 'Exposición', value: 'exposicion' },
|
||||
{ label: 'Deportivo', value: 'deportivo' },
|
||||
{ label: 'Cultural', value: 'cultural' },
|
||||
{ label: 'Otro', value: 'otro' },
|
||||
]}
|
||||
/>
|
||||
{/* Columna Derecha - Detalles del Evento */}
|
||||
<div className="col-lg-6">
|
||||
<div className="ps-lg-3">
|
||||
<h4 className="mb-3">Detalles del Evento</h4>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(evento.fecha_inicio)}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_inicio', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Nombre del evento"
|
||||
required
|
||||
value={evento.nombre_evento}
|
||||
onChange={(e) => handleChange('nombre_evento', e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de fin"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(evento.fecha_fin)}
|
||||
onChange={(e) => handleChange('fecha_fin', new Date(e.target.value))}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Descripción del evento</label>
|
||||
<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={200}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Tipo de evento"
|
||||
value={evento.tipo_evento}
|
||||
placeholder="Selecciona un tipo de evento"
|
||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||
options={tipoEventos}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(evento.fecha_inicio)}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_inicio', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de fin"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(evento.fecha_fin)}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_fin', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,8 +7,11 @@ import {
|
||||
import SimpleInput from '@/components/input';
|
||||
import Select from '@/components/select';
|
||||
import Button from '@/components/button';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import { formatearRangoFechas } from '@/utils/date-utils';
|
||||
|
||||
interface Props {
|
||||
evento?: GetEventoWithCuestionariosWithCupos;
|
||||
formulario: FormularioCreacion;
|
||||
onChange: (formularioActualizado: FormularioCreacion) => void;
|
||||
}
|
||||
@@ -49,7 +52,11 @@ const tiposCuestionario = [
|
||||
{ label: 'Evaluación', value: 2 },
|
||||
];
|
||||
|
||||
export default function FormularioEditor({ formulario, onChange }: Props) {
|
||||
export default function FormularioEditor({
|
||||
formulario,
|
||||
onChange,
|
||||
evento,
|
||||
}: Props) {
|
||||
function actualizarCampo<K extends keyof FormularioCreacion>(
|
||||
campo: K,
|
||||
valor: FormularioCreacion[K]
|
||||
@@ -173,7 +180,7 @@ export default function FormularioEditor({ formulario, onChange }: Props) {
|
||||
<div className="col-md-6">
|
||||
<SimpleInput
|
||||
label="Fecha de Inicio"
|
||||
type="date"
|
||||
type="datetime-local"
|
||||
value={formulario.fecha_inicio.slice(0, 16)}
|
||||
onChange={(e) => actualizarCampo('fecha_inicio', e.target.value)}
|
||||
/>
|
||||
@@ -181,12 +188,18 @@ export default function FormularioEditor({ formulario, onChange }: Props) {
|
||||
<div className="col-md-6">
|
||||
<SimpleInput
|
||||
label="Fecha de Fin"
|
||||
type="date"
|
||||
type="datetime-local"
|
||||
value={formulario.fecha_fin.slice(0, 16)}
|
||||
onChange={(e) => actualizarCampo('fecha_fin', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{evento && (
|
||||
<div className="badge bg-primary fs-6 w-100 mb-3">
|
||||
Este evento se realizara{' '}
|
||||
{formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label="Tipo de Cuestionario"
|
||||
|
||||
+127
-82
@@ -69,93 +69,138 @@ export default function EditEvento({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row border p-3 rounded my-4">
|
||||
<h2>Editar Información del Evento</h2>
|
||||
<p>Modifica los datos del evento que desees actualizar.</p>
|
||||
<div className="my-2">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Editar Información del Evento</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Modifica los datos del evento que desees actualizar y confrimalos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ImageUploader
|
||||
label="Banner del evento"
|
||||
defaultBanner={
|
||||
evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Banner y Nombre */}
|
||||
<div className="col-lg-6">
|
||||
<div className="pe-lg-3">
|
||||
<ImageUploader
|
||||
label="Banner del evento"
|
||||
defaultBanner={
|
||||
evento.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
Esta es la imagen que se verá en el carrusel del evento.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-image me-2"></i>
|
||||
Dimensiones recomendadas: 1200x600 píxeles.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-file-earmark-image me-2"></i>
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Nombre del evento"
|
||||
required
|
||||
value={evento.nombre_evento}
|
||||
onChange={(e) => handleChange('nombre_evento', e.target.value)}
|
||||
/>
|
||||
{/* Columna Derecha - Descripción, Tipo y Fechas */}
|
||||
<div className="col-lg-6">
|
||||
<div className="ps-lg-3">
|
||||
<h4 className="mb-3">Detalles del Evento</h4>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Descripción del evento</label>
|
||||
<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]}
|
||||
/>
|
||||
<Input
|
||||
label="Nombre del evento"
|
||||
required
|
||||
value={evento.nombre_evento}
|
||||
onChange={(e) => handleChange('nombre_evento', e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Descripción del evento</label>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={evento.descripcion_evento ?? ''}
|
||||
onChange={(value) => {
|
||||
const texto = value || '';
|
||||
if (texto.length <= 500) {
|
||||
handleChange('descripcion_evento', texto);
|
||||
}
|
||||
}}
|
||||
height={200}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Tipo de evento"
|
||||
value={evento.tipo_evento}
|
||||
placeholder="Selecciona un tipo de evento"
|
||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||
options={[
|
||||
{ label: 'Conferencia', value: 'conferencia' },
|
||||
{ label: 'Taller', value: 'taller' },
|
||||
{ label: 'Seminario', value: 'seminario' },
|
||||
{ label: 'Curso', value: 'curso' },
|
||||
{ label: 'Webinar', value: 'webinar' },
|
||||
{ label: 'Reunión', value: 'reunion' },
|
||||
{ label: 'Feria', value: 'feria' },
|
||||
{ label: 'Concierto', value: 'concierto' },
|
||||
{ label: 'Exposición', value: 'exposicion' },
|
||||
{ label: 'Deportivo', value: 'deportivo' },
|
||||
{ label: 'Cultural', value: 'cultural' },
|
||||
{ label: 'Otro', value: 'otro' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(evento.fecha_inicio))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_inicio', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de fin"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(evento.fecha_fin))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_fin', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Tipo de evento"
|
||||
value={evento.tipo_evento}
|
||||
placeholder="Selecciona un tipo de evento"
|
||||
onChange={(e) => handleChange('tipo_evento', e.target.value)}
|
||||
options={[
|
||||
{ label: 'Conferencia', value: 'conferencia' },
|
||||
{ label: 'Taller', value: 'taller' },
|
||||
{ label: 'Seminario', value: 'seminario' },
|
||||
{ label: 'Curso', value: 'curso' },
|
||||
{ label: 'Webinar', value: 'webinar' },
|
||||
{ label: 'Reunión', value: 'reunion' },
|
||||
{ label: 'Feria', value: 'feria' },
|
||||
{ label: 'Concierto', value: 'concierto' },
|
||||
{ label: 'Exposición', value: 'exposicion' },
|
||||
{ label: 'Deportivo', value: 'deportivo' },
|
||||
{ label: 'Cultural', value: 'cultural' },
|
||||
{ label: 'Otro', value: 'otro' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(evento.fecha_inicio))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_inicio', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<Input
|
||||
label="Fecha de fin"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(evento.fecha_fin))}
|
||||
onChange={(e) => handleChange('fecha_fin', new Date(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
{/* Botón de guardar en la parte inferior */}
|
||||
<div className="row mt-4">
|
||||
<div className="col-12">
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -65,87 +65,133 @@ export default function EditFormulario({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row border p-3 rounded my-4">
|
||||
<h2>Editar Información del formulario</h2>
|
||||
<p>Modifica los datos del evento que desees actualizar.</p>
|
||||
|
||||
<ImageUploader
|
||||
label="Banner del formulario"
|
||||
defaultBanner={
|
||||
cuestionario.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="col-md-8">
|
||||
<SimpleInput
|
||||
label="Nombre del formulario"
|
||||
required
|
||||
value={cuestionario.nombre_form}
|
||||
onChange={(e) => handleChange('nombre_form', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<SimpleInput
|
||||
label="Cupo máximo (Si no se requiere, dejar en blanco)"
|
||||
type="number"
|
||||
value={cuestionario.cupo_maximo ?? ''}
|
||||
onChange={(e) =>
|
||||
handleChange('cupo_maximo', e.target.value ? e.target.value : '')
|
||||
}
|
||||
/>
|
||||
<div className="my-2">
|
||||
<div className="p-4 border rounded bg-light">
|
||||
<h2 className="mb-2">Editar Información del Formulario</h2>
|
||||
<p className="mb-0 text-muted">
|
||||
Modifica los datos del formulario que desees actualizar y confrimalos.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Descripción del evento</label>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={cuestionario.descripcion ?? ''}
|
||||
onChange={(value) => {
|
||||
const texto = value || '';
|
||||
if (texto.length <= 500) {
|
||||
handleChange(
|
||||
'descripcion' as keyof GetCuestionario,
|
||||
texto
|
||||
);
|
||||
}
|
||||
}}
|
||||
height={250}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="row">
|
||||
{/* Columna Izquierda - Banner */}
|
||||
<div className="col-lg-6">
|
||||
<div className="pe-lg-3">
|
||||
<ImageUploader
|
||||
label="Banner del formulario"
|
||||
defaultBanner={
|
||||
cuestionario.banner
|
||||
? `${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}`
|
||||
: undefined
|
||||
}
|
||||
onChange={(file) => {
|
||||
setNuevoBanner(file);
|
||||
}}
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<strong>
|
||||
<i className="bi bi-info-circle-fill me-2"></i>
|
||||
Esta es la imagen que se usara para la carta de presentación
|
||||
del formulario.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-image me-2"></i>
|
||||
Dimensiones recomendadas: 1200x600 píxeles.
|
||||
</strong>
|
||||
<br />
|
||||
<strong>
|
||||
<i className="bi bi-file-earmark-image me-2"></i>
|
||||
Formatos soportados: PNG, JPG, JPEG.
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columna Derecha - Detalles del formulario */}
|
||||
<div className="col-lg-6">
|
||||
<div className="ps-lg-3">
|
||||
<h4 className="mb-3">Detalles del Formulario</h4>
|
||||
|
||||
<SimpleInput
|
||||
label="Nombre del formulario"
|
||||
required
|
||||
value={cuestionario.nombre_form}
|
||||
onChange={(e) => handleChange('nombre_form', e.target.value)}
|
||||
/>
|
||||
|
||||
<SimpleInput
|
||||
label="Cupo máximo (Si no se requiere, dejar en blanco)"
|
||||
type="number"
|
||||
value={cuestionario.cupo_maximo ?? ''}
|
||||
onChange={(e) =>
|
||||
handleChange(
|
||||
'cupo_maximo',
|
||||
e.target.value ? e.target.value : ''
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="form-label">Descripción del formulario</label>
|
||||
<div data-color-mode="light">
|
||||
<MDEditor
|
||||
value={cuestionario.descripcion ?? ''}
|
||||
onChange={(value) => {
|
||||
const texto = value || '';
|
||||
if (texto.length <= 500) {
|
||||
handleChange(
|
||||
'descripcion' as keyof GetCuestionario,
|
||||
texto
|
||||
);
|
||||
}
|
||||
}}
|
||||
height={200}
|
||||
commands={[commands.bold, commands.italic, commands.hr]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<SimpleInput
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(cuestionario.fecha_inicio))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_inicio', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<SimpleInput
|
||||
label="Fecha de fin"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(cuestionario.fecha_fin))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_fin', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<SimpleInput
|
||||
label="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(cuestionario.fecha_inicio))}
|
||||
onChange={(e) =>
|
||||
handleChange('fecha_inicio', new Date(e.target.value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<SimpleInput
|
||||
label="Fecha de fin"
|
||||
type="datetime-local"
|
||||
className={{ container: 'mb-3' }}
|
||||
value={formatDateLocal(new Date(cuestionario.fecha_fin))}
|
||||
onChange={(e) => handleChange('fecha_fin', new Date(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
{/* Botón de guardar en la parte inferior */}
|
||||
<div className="row mt-4">
|
||||
<div className="col-12">
|
||||
<div className="d-flex justify-content-end">
|
||||
<Button icon="save" disabled={loading} onClick={handleOnSave}>
|
||||
{loading ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import MarkdownRenderer from '@/components/markdown-render';
|
||||
import { useGetApi } from '@/hooks/use-get-api';
|
||||
import { GetCuestionario } from '@/types/evento';
|
||||
import SimpleInput from '@/components/input';
|
||||
@@ -340,13 +339,7 @@ export default function FormularioRegistro({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="preguntas">
|
||||
<hr />
|
||||
<h2 className="text-xl font-bold">{data?.cuestionario.nombre_form}</h2>
|
||||
{data?.cuestionario.descripcion && (
|
||||
<MarkdownRenderer markdown={data.cuestionario.descripcion} />
|
||||
)}
|
||||
|
||||
<div>
|
||||
{preguntaComunidad && (
|
||||
<div className="my-4">
|
||||
<label
|
||||
|
||||
@@ -24,45 +24,186 @@ export default function FormularioPage({
|
||||
|
||||
return (
|
||||
<div className="container my-5">
|
||||
{/* Sección de banners mejorada */}
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
{data?.banner && data?.cuestionario.banner ? (
|
||||
/* Ambos banners disponibles */
|
||||
<div className="d-flex align-items-center justify-content-center gap-3 flex-wrap">
|
||||
<div className="position-relative">
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${data.banner}`}
|
||||
alt={`Banner del evento: ${data.nombre_evento}`}
|
||||
width={350}
|
||||
height={200}
|
||||
className="img-fluid rounded shadow-sm"
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
<div className="position-absolute bottom-0 start-0 m-2">
|
||||
<span className="badge bg-primary bg-opacity-90">
|
||||
<i className="bi bi-calendar-event me-1"></i>
|
||||
Evento
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
className="rounded-circle bg-light border d-flex align-items-center justify-content-center"
|
||||
style={{ width: '50px', height: '50px' }}
|
||||
>
|
||||
<i className="bi bi-x-lg fs-4 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="position-relative">
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${data.cuestionario.banner}`}
|
||||
alt={`Banner del formulario: ${data.cuestionario.nombre_form}`}
|
||||
width={350}
|
||||
height={200}
|
||||
className="img-fluid rounded shadow-sm"
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
<div className="position-absolute bottom-0 start-0 m-2">
|
||||
<span className="badge bg-success bg-opacity-90">
|
||||
<i className="bi bi-file-earmark-text me-1"></i>
|
||||
Formulario
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : data?.banner ? (
|
||||
/* Solo banner del evento */
|
||||
<div>
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${data.banner}`}
|
||||
alt={data?.nombre_evento || ''}
|
||||
width={800}
|
||||
height={400}
|
||||
className="img-fluid rounded shadow"
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<i className="bi bi-calendar-event me-1"></i>
|
||||
Banner del evento
|
||||
</p>
|
||||
</div>
|
||||
) : data?.cuestionario.banner ? (
|
||||
/* Solo banner del formulario */
|
||||
<div>
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${data.cuestionario.banner}`}
|
||||
alt={data?.cuestionario.nombre_form || ''}
|
||||
width={800}
|
||||
height={400}
|
||||
className="img-fluid rounded shadow"
|
||||
/>
|
||||
<p className="text-muted mt-2 small">
|
||||
<i className="bi bi-file-earmark-text me-1"></i>
|
||||
Banner del formulario
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<h1>{data?.nombre_evento}</h1>
|
||||
{data?.descripcion_evento && (
|
||||
<MarkdownRenderer markdown={data?.descripcion_evento} />
|
||||
)}
|
||||
|
||||
{data?.fecha_fin && data?.fecha_inicio && (
|
||||
<>
|
||||
<p className="mb-0 mt-2 text-muted">Horario:</p>
|
||||
<p className="text-muted">
|
||||
<i className="bi bi-clock me-1 text-primary"></i>
|
||||
{formatFecha(data.fecha_inicio!, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}{' '}
|
||||
al{' '}
|
||||
{formatFecha(data.fecha_fin!, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{/* Layout de dos columnas */}
|
||||
<div className="row">
|
||||
{/* Columna izquierda - Información */}
|
||||
<div className="col-lg-5">
|
||||
{/* Información del evento */}
|
||||
<div className="mb-4">
|
||||
<h1 className="mb-3">{data?.nombre_evento}</h1>
|
||||
{data?.descripcion_evento && (
|
||||
<div className="mb-3">
|
||||
<MarkdownRenderer markdown={data?.descripcion_evento} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<FormularioRegistro
|
||||
id_cuestionario={data?.cuestionario.id_cuestionario}
|
||||
/>
|
||||
)}
|
||||
{data?.fecha_fin && data?.fecha_inicio && (
|
||||
<div className="mb-3">
|
||||
<p className="mb-1 fw-semibold text-muted">
|
||||
<i className="bi bi-calendar-event me-2"></i>
|
||||
Horario del evento:
|
||||
</p>
|
||||
<p className="text-muted ps-4">
|
||||
<i className="bi bi-clock me-1 text-primary"></i>
|
||||
{formatFecha(data.fecha_inicio!, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}{' '}
|
||||
al{' '}
|
||||
{formatFecha(data.fecha_fin!, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Información del formulario */}
|
||||
{data?.cuestionario && (
|
||||
<div className="card border-0 bg-light">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title text-primary mb-3">
|
||||
<i className="bi bi-file-earmark-text me-2"></i>
|
||||
Registro al formulario
|
||||
</h5>
|
||||
|
||||
<div className="mb-3">
|
||||
<h6 className="fw-bold mb-2">Te estás registrando a:</h6>
|
||||
<p className="mb-1">
|
||||
<span className="badge bg-primary me-2">Formulario</span>
|
||||
<strong>{data.cuestionario.nombre_form}</strong>
|
||||
</p>
|
||||
<p className="mb-0 text-muted small">
|
||||
<span className="badge bg-secondary me-2">Evento</span>
|
||||
{data.nombre_evento}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{data.cuestionario.descripcion && (
|
||||
<div className="mb-3">
|
||||
<h6 className="fw-bold mb-2">Descripción:</h6>
|
||||
<MarkdownRenderer
|
||||
markdown={data.cuestionario.descripcion}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.cuestionario.fecha_inicio &&
|
||||
data.cuestionario.fecha_fin && (
|
||||
<div className="mb-0">
|
||||
<h6 className="fw-bold mb-2">Horario del formulario:</h6>
|
||||
<p className="text-muted small mb-0">
|
||||
<i className="bi bi-clock me-1"></i>
|
||||
{formatFecha(data.cuestionario.fecha_inicio, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}{' '}
|
||||
al{' '}
|
||||
{formatFecha(data.cuestionario.fecha_fin, {
|
||||
horas: true,
|
||||
minutos: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Columna derecha - Formulario */}
|
||||
<div className="col-lg-7">
|
||||
<div className="ps-lg-4">
|
||||
{data && (
|
||||
<FormularioRegistro
|
||||
id_cuestionario={data?.cuestionario.id_cuestionario}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
import { GetEventoWithCuestionariosWithCupos } from '@/types/evento';
|
||||
import axiosInstance from '@/utils/api-config';
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
|
||||
interface EventoContextType {
|
||||
evento: GetEventoWithCuestionariosWithCupos | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => void;
|
||||
updateEvento: (
|
||||
eventoData: Partial<GetEventoWithCuestionariosWithCupos>
|
||||
) => void;
|
||||
setEventoData: (evento: GetEventoWithCuestionariosWithCupos) => void;
|
||||
}
|
||||
|
||||
const EventoContext = createContext<EventoContextType | undefined>(undefined);
|
||||
|
||||
interface EventoProviderProps {
|
||||
children: ReactNode;
|
||||
id_evento: string;
|
||||
}
|
||||
|
||||
export function EventoProvider({ children, id_evento }: EventoProviderProps) {
|
||||
const [evento, setEvento] =
|
||||
useState<GetEventoWithCuestionariosWithCupos | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchEvento = useCallback(async () => {
|
||||
console.log('Fetching evento with ID:', id_evento);
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response =
|
||||
await axiosInstance.get<GetEventoWithCuestionariosWithCupos>(
|
||||
`/evento/${id_evento}/cuestionarios`
|
||||
);
|
||||
setEvento(response.data);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Error al cargar el evento';
|
||||
setError(errorMessage);
|
||||
console.error('Error fetching evento:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id_evento]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id_evento) {
|
||||
fetchEvento();
|
||||
}
|
||||
}, [id_evento, fetchEvento]);
|
||||
|
||||
const refetch = () => {
|
||||
fetchEvento();
|
||||
};
|
||||
|
||||
const updateEvento = useCallback(
|
||||
(eventoData: Partial<GetEventoWithCuestionariosWithCupos>) => {
|
||||
setEvento((prev) => (prev ? { ...prev, ...eventoData } : null));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const setEventoData = useCallback(
|
||||
(newEvento: GetEventoWithCuestionariosWithCupos) => {
|
||||
setEvento(newEvento);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const value: EventoContextType = {
|
||||
evento,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
updateEvento,
|
||||
setEventoData,
|
||||
};
|
||||
|
||||
return (
|
||||
<EventoContext.Provider value={value}>{children}</EventoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useEvento() {
|
||||
const context = useContext(EventoContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useEvento must be used within an EventoProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './evento-context';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './evento';
|
||||
Vendored
+4
@@ -174,3 +174,7 @@ input {
|
||||
margin-inline: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table-white {
|
||||
--bs-table-bg: #fff;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@ export interface GetEvento {
|
||||
fecha_fin: string; // formato ISO 8601
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
asistencias: any | null;
|
||||
banner: string | null;
|
||||
banner: string | null; // banner-1755043351827-446638467.jpeg
|
||||
}
|
||||
|
||||
export interface Cuestionario {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const tipoEventos = [
|
||||
{ label: 'Conferencia', value: 'conferencia' },
|
||||
{ label: 'Taller', value: 'taller' },
|
||||
{ label: 'Seminario', value: 'seminario' },
|
||||
{ label: 'Curso', value: 'curso' },
|
||||
{ label: 'Webinar', value: 'webinar' },
|
||||
{ label: 'Reunión', value: 'reunion' },
|
||||
{ label: 'Feria', value: 'feria' },
|
||||
{ label: 'Concierto', value: 'concierto' },
|
||||
{ label: 'Exposición', value: 'exposicion' },
|
||||
{ label: 'Deportivo', value: 'deportivo' },
|
||||
{ label: 'Cultural', value: 'cultural' },
|
||||
{ label: 'Otro', value: 'otro' },
|
||||
];
|
||||
+90
-1
@@ -28,7 +28,6 @@
|
||||
export function formatFecha(
|
||||
fechaStr: string,
|
||||
config: { horas?: boolean; minutos?: boolean; diaSemana?: boolean } = {
|
||||
|
||||
horas: false,
|
||||
minutos: false,
|
||||
diaSemana: false,
|
||||
@@ -104,3 +103,93 @@ export function formatDateLocal(date: Date): string {
|
||||
|
||||
return `${yyyy}-${MM}-${dd}T${hh}:${mm}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea un rango de fechas en español, optimizando la salida según si coinciden año, mes y día.
|
||||
*
|
||||
* @param {string} fechaInicio - Fecha de inicio en formato ISO 8601 (ej: '2025-08-13T18:44:00.000Z').
|
||||
* @param {string} fechaFin - Fecha de fin en formato ISO 8601 (ej: '2025-08-15T18:44:00.000Z').
|
||||
* @returns {string} Rango de fechas formateado en español.
|
||||
* @throws {Error} Lanza un error si alguna de las fechas proporcionadas no es válida.
|
||||
*
|
||||
* @example
|
||||
* formatearRangoFechas('2025-08-13T12:00:00.000Z', '2025-08-13T18:00:00.000Z');
|
||||
* // Retorna: "el 13 de agosto del 2025 de 12:00 a 18:00"
|
||||
*
|
||||
* @example
|
||||
* formatearRangoFechas('2025-08-13T18:44:00.000Z', '2025-08-15T18:44:00.000Z');
|
||||
* // Retorna: "del 13 al 15 de agosto del 2025"
|
||||
*
|
||||
* @example
|
||||
* formatearRangoFechas('2025-08-13T18:44:00.000Z', '2025-09-13T18:44:00.000Z');
|
||||
* // Retorna: "del 13 de agosto al 13 de septiembre del 2025"
|
||||
*
|
||||
* @example
|
||||
* formatearRangoFechas('2025-08-13T18:44:00.000Z', '2026-01-13T18:44:00.000Z');
|
||||
* // Retorna: "del 13 de agosto del 2025 al 13 de enero del 2026"
|
||||
*/
|
||||
export function formatearRangoFechas(
|
||||
fechaInicio: string,
|
||||
fechaFin: string
|
||||
): string {
|
||||
const inicio = new Date(fechaInicio);
|
||||
const fin = new Date(fechaFin);
|
||||
|
||||
if (isNaN(inicio.getTime())) {
|
||||
throw new Error(`Fecha de inicio inválida: ${fechaInicio}`);
|
||||
}
|
||||
|
||||
if (isNaN(fin.getTime())) {
|
||||
throw new Error(`Fecha de fin inválida: ${fechaFin}`);
|
||||
}
|
||||
|
||||
const meses = [
|
||||
'enero',
|
||||
'febrero',
|
||||
'marzo',
|
||||
'abril',
|
||||
'mayo',
|
||||
'junio',
|
||||
'julio',
|
||||
'agosto',
|
||||
'septiembre',
|
||||
'octubre',
|
||||
'noviembre',
|
||||
'diciembre',
|
||||
];
|
||||
|
||||
const diaInicio = inicio.getUTCDate();
|
||||
const mesInicio = meses[inicio.getUTCMonth()];
|
||||
const añoInicio = inicio.getUTCFullYear();
|
||||
|
||||
const diaFin = fin.getUTCDate();
|
||||
const mesFin = meses[fin.getUTCMonth()];
|
||||
const añoFin = fin.getUTCFullYear();
|
||||
|
||||
// Si es el mismo día (año, mes y día iguales)
|
||||
if (
|
||||
añoInicio === añoFin &&
|
||||
inicio.getUTCMonth() === fin.getUTCMonth() &&
|
||||
diaInicio === diaFin
|
||||
) {
|
||||
const horaInicio = (inicio.getUTCHours() - 6 + 24) % 24;
|
||||
const minutosInicio = inicio.getUTCMinutes().toString().padStart(2, '0');
|
||||
const horaFin = (fin.getUTCHours() - 6 + 24) % 24;
|
||||
const minutosFin = fin.getUTCMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `el ${diaInicio} de ${mesInicio} del ${añoInicio} de ${horaInicio}:${minutosInicio} a ${horaFin}:${minutosFin}`;
|
||||
}
|
||||
|
||||
// Si el año y mes son iguales pero días diferentes
|
||||
if (añoInicio === añoFin && inicio.getUTCMonth() === fin.getUTCMonth()) {
|
||||
return `del ${diaInicio} al ${diaFin} de ${mesInicio} del ${añoInicio}`;
|
||||
}
|
||||
|
||||
// Si el año es igual pero el mes es diferente
|
||||
if (añoInicio === añoFin) {
|
||||
return `del ${diaInicio} de ${mesInicio} al ${diaFin} de ${mesFin} del ${añoInicio}`;
|
||||
}
|
||||
|
||||
// Si tanto el año como el mes son diferentes
|
||||
return `del ${diaInicio} de ${mesInicio} del ${añoInicio} al ${diaFin} de ${mesFin} del ${añoFin}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// slugify.ts
|
||||
export function slugify(input: string, maxLen = 60) {
|
||||
return input
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, maxLen);
|
||||
}
|
||||
|
||||
// Combina slug + id (id al final facilita parsear con "último -")
|
||||
export function toParam(name: string, id: string | number) {
|
||||
return `${slugify(name)}-${id}`;
|
||||
}
|
||||
|
||||
// Extrae { id, slug } de un param con formato "slug-123"
|
||||
export function fromParam(param: string) {
|
||||
const lastDash = param.lastIndexOf('-');
|
||||
if (lastDash === -1) return { slug: param, id: null };
|
||||
const slug = param.slice(0, lastDash);
|
||||
const idStr = param.slice(lastDash + 1);
|
||||
const id = /^\d+$/.test(idStr) ? Number(idStr) : null;
|
||||
return { slug, id };
|
||||
}
|
||||
Reference in New Issue
Block a user