diff --git a/public/banner3.png b/public/banner3.png new file mode 100644 index 0000000..304330e Binary files /dev/null and b/public/banner3.png differ diff --git a/src/app/(admins)/administrador/crear-evento/page.tsx b/src/app/(admins)/administrador/crear-evento/page.tsx deleted file mode 100644 index 2affa1b..0000000 --- a/src/app/(admins)/administrador/crear-evento/page.tsx +++ /dev/null @@ -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(null); - const [evento, setEvento] = useState({ - tipo_evento: '', - nombre_evento: '', - descripcion_evento: '', - fecha_inicio: new Date(), - fecha_fin: new Date(), - }); - const router = useRouter(); - - const [datosFormulario, setDatosFormulario] = - useState(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 ( -
-
-

Crear evento y formulario de registro

-

- En esta pagina podras crear un evento y un primer formulario de - registro, posteriormente podras crear mas formularios relacionados a - este evento. -

-
- - - - {!datosFormulario && ( -
-

- Selecciona una plantilla para el formulario: -

-

- Elige una de las plantillas disponibles para crear tu formulario de - registro. -
- 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. -
- - Para asegurar el funcionamiento óptimo del prellenado, modifica la - estructura solo si es necesario. - -

- -
- {plantillasDisponibles.map((plantilla) => ( -
handleSeleccionarPlantilla(plantilla.id)} - > -
- {`Plantilla -
-
- ))} -
-
- )} - - {datosFormulario && ( - setDatosFormulario({ ...nuevo })} - /> - )} - - -
- ); -} diff --git a/src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx b/src/app/(admins)/administrador/evento/[id_evento]/formulario/[id_cuestionario]/page.tsx similarity index 91% rename from src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx rename to src/app/(admins)/administrador/evento/[id_evento]/formulario/[id_cuestionario]/page.tsx index c486950..bb8a754 100644 --- a/src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx +++ b/src/app/(admins)/administrador/evento/[id_evento]/formulario/[id_cuestionario]/page.tsx @@ -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(); + const { evento } = useEvento(); const [cuestionario, setCuestionario] = React.useState(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 (
- - - Volver - - -

Formulario

+ {cuestionario && ( (); + const router = useRouter(); + const { evento, refetch } = useEvento(); + const [datosFormulario, setDatosFormulario] = + useState(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 ( +
+ + +
+

Creación de formulario

+

+ 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. +

+
+ {!datosFormulario && ( +
+

+ Selecciona una plantilla para el formulario: +

+

+ Elige una de las plantillas disponibles para crear tu formulario de + registro. +
+ 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. +
+ + Para asegurar el funcionamiento óptimo del prellenado, modifica la + estructura solo si es necesario. + +

+ +
+ {plantillasDisponibles.map((plantilla) => ( +
handleSeleccionarPlantilla(plantilla.id)} + > +
+ {`Plantilla +
+
+ ))} +
+
+ )} + {datosFormulario && ( + setDatosFormulario({ ...nuevo })} + /> + )} + +
+ ); +} diff --git a/src/app/(admins)/administrador/evento/[id_evento]/layout.tsx b/src/app/(admins)/administrador/evento/[id_evento]/layout.tsx new file mode 100644 index 0000000..b82638e --- /dev/null +++ b/src/app/(admins)/administrador/evento/[id_evento]/layout.tsx @@ -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(); + + if (!params.id_evento) { + return
Error: ID de evento no encontrado
; + } + + return ( + {children} + ); +} diff --git a/src/app/(admins)/administrador/evento/[id_evento]/page.tsx b/src/app/(admins)/administrador/evento/[id_evento]/page.tsx index aaf3b44..8181bbc 100644 --- a/src/app/(admins)/administrador/evento/[id_evento]/page.tsx +++ b/src/app/(admins)/administrador/evento/[id_evento]/page.tsx @@ -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(); - const { data } = useGetApi( - `/evento/${params.id_evento}/cuestionarios` + const { evento, loading, error, updateEvento, setEventoData } = useEvento(); + + const [loadingStates, setLoadingStates] = useState>( + {} ); - const [evento, setEvento] = - useState(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[] = [ - { - 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) => ( -
- - Ver/Editar formulario - -
- ), - }, - { - key: 'banner', - label: 'Descargar respuestas', - render: (_, row) => ( - - ), - }, - ]; - - if (!evento) return

Cargando...

; + if (loading) return

Cargando...

; + if (error) return

Error: {error}

; + if (!evento) return

No se encontró el evento

; 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 (
- - - Volver - - -

Evento

+ - {data?.cuestionarios && data.cuestionarios.length > 0 && ( - <> -

Cuestionarios del evento

-
- +
+
+
+

Formularios del evento

+

+ Aquí puedes ver y gestionar los formularios asociados a este + evento. +

- + +
+
+ + {evento?.cuestionarios && evento.cuestionarios.length > 0 && ( +
+ {evento.cuestionarios.map((item, key) => { + const fadeClass = `delay-${(key % 5) + 1}`; + return ( +
+ +
+ ); + })} +
)} ); diff --git a/src/app/(admins)/administrador/eventos/crear/page.tsx b/src/app/(admins)/administrador/eventos/crear/page.tsx new file mode 100644 index 0000000..3bc334f --- /dev/null +++ b/src/app/(admins)/administrador/eventos/crear/page.tsx @@ -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(null); + const [evento, setEvento] = useState({ + 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 ( +
+ + + {/* Botón de guardar en la parte inferior */} +
+
+
+ +
+
+
+
+ ); +} diff --git a/src/app/(admins)/administrador/page.tsx b/src/app/(admins)/administrador/eventos/page.tsx similarity index 86% rename from src/app/(admins)/administrador/page.tsx rename to src/app/(admins)/administrador/eventos/page.tsx index cbf8b05..699ce1e 100644 --- a/src/app/(admins)/administrador/page.tsx +++ b/src/app/(admins)/administrador/eventos/page.tsx @@ -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( '/evento/recientes/cuestionarios' ); - const { data: eventos } = useGetApi('/evento/activos'); if (loading) return
Cargando formularios...
; if (error) return
{error.message}
; @@ -23,13 +23,14 @@ export default function Page() { <>

Eventos

- +
{loading &&
Cargando...
} - {data && ( + {data && data.length > 0 && (
+

Eventos Activos

{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} > - +
); })} )} - {recientes && ( + {recientes && recientes.length > 0 && (

Eventos Recientes

Eventos de los ultimos 30 días.

@@ -60,7 +61,6 @@ export default function Page() { })}
)} -
{JSON.stringify(eventos, null, 2)}
); } diff --git a/src/app/(auth)/login/administradores/page.tsx b/src/app/(auth)/login/administradores/page.tsx index 7b582df..6285f2e 100644 --- a/src/app/(auth)/login/administradores/page.tsx +++ b/src/app/(auth)/login/administradores/page.tsx @@ -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'); } diff --git a/src/app/(public)/evento/[evento]/[cuestionario]/page.tsx b/src/app/(public)/evento/[evento]/[cuestionario]/page.tsx new file mode 100644 index 0000000..b14afc0 --- /dev/null +++ b/src/app/(public)/evento/[evento]/[cuestionario]/page.tsx @@ -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; +}; + +export async function generateMetadata({ params }: Props): Promise { + 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 ( + + ); +} diff --git a/src/app/(public)/evento/[nombre_evento]/[id_evento]/[id_cuestionario]/page.tsx b/src/app/(public)/evento/[nombre_evento]/[id_evento]/[id_cuestionario]/page.tsx deleted file mode 100644 index ba0203c..0000000 --- a/src/app/(public)/evento/[nombre_evento]/[id_evento]/[id_cuestionario]/page.tsx +++ /dev/null @@ -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; -}; - -export async function generateMetadata({ params }: Props): Promise { - 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 ( - - ); -} diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 7381364..13c6ed6 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -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() {
({ + src: `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`, + alt: `Banner de ${evento.nombre_evento}`, + })) + : [ + { + src: '/banner.jpeg', + alt: 'Banner de eventos activos', + }, + ] + } />
{loading &&
Cargando...
} {data && (
- {data.map((item, key) => { - const fadeClass = `delay-${(key % 5) + 1}`; - return ( -
- -
- ); - })} + {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 ( +
+ +
+ ); + }) + )}
)}
diff --git a/src/components/banner-uploader.tsx b/src/components/banner-uploader.tsx index 32aa635..6f77c0b 100644 --- a/src/components/banner-uploader.tsx +++ b/src/components/banner-uploader.tsx @@ -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(null); const [preview, setPreview] = useState(defaultBanner || null); @@ -41,17 +43,28 @@ export default function ImageUploader({ }; return ( -
+
{label && } - +
+ + {preview && ( + + )} +
{preview && ( -
+
-
)}
diff --git a/src/components/breadcrumb.tsx b/src/components/breadcrumb.tsx new file mode 100644 index 0000000..dd05474 --- /dev/null +++ b/src/components/breadcrumb.tsx @@ -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 ( + + ); +} diff --git a/src/components/cuestionario-card.tsx b/src/components/cuestionario-card.tsx index 161b4dc..d615ab4 100644 --- a/src/components/cuestionario-card.tsx +++ b/src/components/cuestionario-card.tsx @@ -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', }: { diff --git a/src/components/evento-card.tsx b/src/components/evento-card.tsx index 1499a09..595124e 100644 --- a/src/components/evento-card.tsx +++ b/src/components/evento-card.tsx @@ -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
EventoCard
; +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 ( +
+
+
+ + +
{evento.nombre_evento}
+ +
+
+
+ + {descripcion.length > limite && ( + + )} +
+ {evento.cuestionarios.length > 0 ? ( + <> +

Formularios

+
+ + + + + + + + + {evento.cuestionarios.map((cuestionario) => ( + + + {cuestionario.cupo_maximo ? ( + <> + + + + ) : ( + + )} + + ))} + +
NombreInscritosCapacidad total
+ + {cuestionario.nombre_form} + + {cuestionario.cupos_usados}{cuestionario.cupo_maximo} + Sin límite +
+ + ) : ( +
+ +

+ Este evento no tiene formularios disponibles. +

+ {(user === 'administrador' || user === 'staff') && ( + + + Crear formulario + + )} +
+ )} + + Evento {formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)} + + + {/* Botón único del evento */} +
+ {user === 'administrador' || user === 'staff' ? ( + + Ver/Editar evento + + ) : evento.cuestionarios.length === 0 ? ( + + ) : evento.cuestionarios.some( + (cuestionario) => + cuestionario.cupos_disponibles === 0 && + cuestionario.cupo_maximo !== null + ) && + evento.cuestionarios.every( + (cuestionario) => + cuestionario.cupos_disponibles === 0 && + cuestionario.cupo_maximo !== null + ) ? ( + + ) : ( + + Registro + + )} +
+
+
+
+ ); } diff --git a/src/components/evento/evento-card-admin.tsx b/src/components/evento/evento-card-admin.tsx new file mode 100644 index 0000000..1f6e1c2 --- /dev/null +++ b/src/components/evento/evento-card-admin.tsx @@ -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 ( +
+
+ + Banner formulario + + + {/* Badge de cantidad de formularios en la esquina superior derecha */} +
+ {evento.cuestionarios.length > 0 ? ( + + {evento.cuestionarios.length} formulario + {evento.cuestionarios.length !== 1 ? 's' : ''} + + ) : ( + Sin formularios + )} +
+
+ +
+ {/* Fecha del evento */} +
+
+
+ {new Date(evento.fecha_inicio) + .toLocaleDateString('es-ES', { month: 'short' }) + .toUpperCase()} +
+
+ {(() => { + 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}`; + })()} +
+
+
+
{evento.nombre_evento}
+
+
+ + {/* Descripción */} +
+ + {descripcion.length > limite && ( + + )} +
+ + {/* Formularios/sub eventos */} + {evento.cuestionarios.length > 0 ? ( +
+
+ Formularios disponibles: +
+
+ {evento.cuestionarios.map((cuestionario) => ( +
+
+
+ + {cuestionario.nombre_form} + + + {cuestionario.cupo_maximo + ? `${cuestionario.cupos_usados}/${cuestionario.cupo_maximo}` + : 'Sin límite'} + +
+
+
+ ))} +
+
+ ) : ( +
+ +

Sin formularios disponibles

+ + + Crear formulario + +
+ )} + + {/* Botón principal */} + + Ver/Editar evento + +
+
+ ); +} diff --git a/src/components/evento/evento-card-user.tsx b/src/components/evento/evento-card-user.tsx new file mode 100644 index 0000000..1bd3071 --- /dev/null +++ b/src/components/evento/evento-card-user.tsx @@ -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 ( +
+
+
+ + Banner formulario +
{evento.nombre_evento}
+ +
+
+
+ + {descripcion.length > limite && ( + + )} +
+ {evento.cuestionarios.length > 0 ? ( + <> +

Formularios

+ + + + + + + + + + {evento.cuestionarios.map((cuestionario) => ( + + + {cuestionario.cupo_maximo ? ( + <> + + + + ) : ( + + )} + + ))} + +
NombreInscritosCapacidad total
+ + {cuestionario.nombre_form} + + {cuestionario.cupos_usados}{cuestionario.cupo_maximo} + Sin límite +
+ + ) : ( +
+ +

+ Este evento no tiene formularios disponibles. +

+ {(user === 'administrador' || user === 'staff') && ( + + + Crear formulario + + )} +
+ )} + + Evento {formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)} + + + {/* Botón único del evento */} +
+ {user === 'administrador' || user === 'staff' ? ( + + Ver/Editar evento + + ) : evento.cuestionarios.length === 0 ? ( + + ) : evento.cuestionarios.some( + (cuestionario) => + cuestionario.cupos_disponibles === 0 && + cuestionario.cupo_maximo !== null + ) && + evento.cuestionarios.every( + (cuestionario) => + cuestionario.cupos_disponibles === 0 && + cuestionario.cupo_maximo !== null + ) ? ( + + ) : ( + + Registro + + )} +
+
+
+
+ ); +} diff --git a/src/components/formulario/formulario-card-admin.tsx b/src/components/formulario/formulario-card-admin.tsx new file mode 100644 index 0000000..b82cc10 --- /dev/null +++ b/src/components/formulario/formulario-card-admin.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function FormularioCardAdmin() { + return
FormularioCardAdmin
; +} diff --git a/src/components/formulario/formulario-card-user.tsx b/src/components/formulario/formulario-card-user.tsx new file mode 100644 index 0000000..7cb0715 --- /dev/null +++ b/src/components/formulario/formulario-card-user.tsx @@ -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 ( +
+
+ + Banner formulario + + + {/* Badge de cupos en la esquina superior derecha */} +
+ {formulario.cupo_maximo === null ? ( + Sin límite + ) : ( + + {formulario.cupos_disponibles} cupos + + )} +
+
+ +
+ {/* Fecha del evento */} +
+
+
+ {new Date(formulario.fecha_inicio) + .toLocaleDateString('es-ES', { month: 'short' }) + .toUpperCase()} +
+
+ {(() => { + 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}`; + })()} +
+
+
+ + {evento.nombre_evento} + +
+ {formulario.nombre_form} +
+
+
+ + {/* Descripción */} +
+ + {descripcion.length > limite && ( + + )} +
+ + {/* Botón de registro */} + + Registro + +
+
+ ); +} diff --git a/src/components/new-cuestionario-card.tsx b/src/components/new-cuestionario-card.tsx new file mode 100644 index 0000000..a80a486 --- /dev/null +++ b/src/components/new-cuestionario-card.tsx @@ -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 ( +
+
+
+ + Banner formulario +
{cuestionario.nombre_form}
+ +
+
+ +
+
+ {cuestionario.cupo_maximo === null ? ( + Sin límite + ) : ( + + {cuestionario.cupos_disponibles} cupos disponibles + + )} + + {descripcion.length > limite && ( + + )} +
+ + Evento{' '} + {formatearRangoFechas( + cuestionario.fecha_inicio, + cuestionario.fecha_fin + )} + +
+ {user === 'administrador' || user === 'staff' ? ( + <> + + Ver formulario + + {user === 'administrador' && onDownload && ( + + )} + + ) : cuestionario.cupos_disponibles === 0 && + cuestionario.cupo_maximo !== null ? ( + + ) : ( + + Registro + + )} +
+
+
+
+ ); +} diff --git a/src/containers/create-evento.tsx b/src/containers/create-evento.tsx index 4885b75..4b043fe 100644 --- a/src/containers/create-evento.tsx +++ b/src/containers/create-evento.tsx @@ -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 ( -
-

Información del Evento

-

- Completa la siguiente información para describir los detalles generales - del evento. -

+
+
+

Creación del Evento

+

+ Completa la siguiente información para describir los detalles + generales del evento. +

+

+ Presiona siguiente para continuar con la creacion del formulario. +

+
- - - handleChange('nombre_evento', e.target.value)} - /> - -
- -
- { - const texto = value || ''; - if (texto.length <= MAX_LENGTH) { - handleChange('descripcion_evento', texto); - } - }} - height={300} - commands={[commands.bold, commands.italic, commands.hr]} - /> +
+ {/* Columna Izquierda - Banner */} +
+
+ +

+ + + Esta es la imagen que se verá en el carrusel del evento. + +
+ + + Dimensiones recomendadas: 1200x600 píxeles. + +
+ + + Formatos soportados: PNG, JPG, JPEG. + +

+
-
- - handleChange('fecha_inicio', new Date(e.target.value)) - } - /> -
+ handleChange('nombre_evento', e.target.value)} + /> -
- handleChange('fecha_fin', new Date(e.target.value))} - /> +
+ +
+ { + const texto = value || ''; + if (texto.length <= MAX_LENGTH) { + handleChange('descripcion_evento', texto); + } + }} + height={200} + commands={[commands.bold, commands.italic, commands.hr]} + /> +
+
+ + + handleChange('fecha_inicio', new Date(e.target.value)) + } + /> +
+ +
+ + handleChange('fecha_fin', new Date(e.target.value)) + } + /> +
+
+
+
); diff --git a/src/containers/create-formulario.tsx b/src/containers/create-formulario.tsx index 458a1fa..714a5da 100644 --- a/src/containers/create-formulario.tsx +++ b/src/containers/create-formulario.tsx @@ -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( campo: K, valor: FormularioCreacion[K] @@ -173,7 +180,7 @@ export default function FormularioEditor({ formulario, onChange }: Props) {
actualizarCampo('fecha_inicio', e.target.value)} /> @@ -181,12 +188,18 @@ export default function FormularioEditor({ formulario, onChange }: Props) {
actualizarCampo('fecha_fin', e.target.value)} />
+ {evento && ( +
+ Este evento se realizara{' '} + {formatearRangoFechas(evento.fecha_inicio, evento.fecha_fin)} +
+ )} handleChange('nombre_evento', e.target.value)} - /> + {/* Columna Derecha - Descripción, Tipo y Fechas */} +
+
+

Detalles del Evento

-
- -
- { - const texto = value || ''; - if (texto.length <= 500) { - handleChange('descripcion_evento', texto); - } - }} - height={250} - commands={[commands.bold, commands.italic, commands.hr]} - /> + handleChange('nombre_evento', e.target.value)} + /> + +
+ +
+ { + const texto = value || ''; + if (texto.length <= 500) { + handleChange('descripcion_evento', texto); + } + }} + height={200} + commands={[commands.bold, commands.italic, commands.hr]} + /> +
+
+ + + handleChange('fecha_inicio', new Date(e.target.value)) + } + /> +
+ +
+ + handleChange('fecha_fin', new Date(e.target.value)) + } + /> +
+
+
+
- - - handleChange('fecha_inicio', new Date(e.target.value)) - } - /> - - -
- handleChange('fecha_fin', new Date(e.target.value))} - /> -
- -
- + {/* Botón de guardar en la parte inferior */} +
+
+
+ +
+
+
); diff --git a/src/containers/edit-formulario.tsx b/src/containers/edit-formulario.tsx index bed9a48..33c4b15 100644 --- a/src/containers/edit-formulario.tsx +++ b/src/containers/edit-formulario.tsx @@ -65,87 +65,133 @@ export default function EditFormulario({ }; return ( -
-

Editar Información del formulario

-

Modifica los datos del evento que desees actualizar.

- - { - setNuevoBanner(file); - }} - /> - -
- handleChange('nombre_form', e.target.value)} - /> -
-
- - handleChange('cupo_maximo', e.target.value ? e.target.value : '') - } - /> +
+
+

Editar Información del Formulario

+

+ Modifica los datos del formulario que desees actualizar y confrimalos. +

-
- -
- { - const texto = value || ''; - if (texto.length <= 500) { - handleChange( - 'descripcion' as keyof GetCuestionario, - texto - ); - } - }} - height={250} - commands={[commands.bold, commands.italic, commands.hr]} - /> +
+
+ {/* Columna Izquierda - Banner */} +
+
+ { + setNuevoBanner(file); + }} + /> +

+ + + Esta es la imagen que se usara para la carta de presentación + del formulario. + +
+ + + Dimensiones recomendadas: 1200x600 píxeles. + +
+ + + Formatos soportados: PNG, JPG, JPEG. + +

+
+
+ + {/* Columna Derecha - Detalles del formulario */} +
+
+

Detalles del Formulario

+ + handleChange('nombre_form', e.target.value)} + /> + + + handleChange( + 'cupo_maximo', + e.target.value ? e.target.value : '' + ) + } + /> + +
+ +
+ { + const texto = value || ''; + if (texto.length <= 500) { + handleChange( + 'descripcion' as keyof GetCuestionario, + texto + ); + } + }} + height={200} + commands={[commands.bold, commands.italic, commands.hr]} + /> +
+
+ +
+
+ + handleChange('fecha_inicio', new Date(e.target.value)) + } + /> +
+ +
+ + handleChange('fecha_fin', new Date(e.target.value)) + } + /> +
+
+
+
-
-
- - handleChange('fecha_inicio', new Date(e.target.value)) - } - /> -
- -
- handleChange('fecha_fin', new Date(e.target.value))} - /> -
- -
- + {/* Botón de guardar en la parte inferior */} +
+
+
+ +
+
+
); diff --git a/src/containers/formulario/formulario-registro.tsx b/src/containers/formulario/formulario-registro.tsx index 11d9288..992b3ac 100644 --- a/src/containers/formulario/formulario-registro.tsx +++ b/src/containers/formulario/formulario-registro.tsx @@ -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 ( -
-
-

{data?.cuestionario.nombre_form}

- {data?.cuestionario.descripcion && ( - - )} - +
{preguntaComunidad && (
); } diff --git a/src/context/evento/evento-context.tsx b/src/context/evento/evento-context.tsx new file mode 100644 index 0000000..1e52318 --- /dev/null +++ b/src/context/evento/evento-context.tsx @@ -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 + ) => void; + setEventoData: (evento: GetEventoWithCuestionariosWithCupos) => void; +} + +const EventoContext = createContext(undefined); + +interface EventoProviderProps { + children: ReactNode; + id_evento: string; +} + +export function EventoProvider({ children, id_evento }: EventoProviderProps) { + const [evento, setEvento] = + useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchEvento = useCallback(async () => { + console.log('Fetching evento with ID:', id_evento); + try { + setLoading(true); + setError(null); + const response = + await axiosInstance.get( + `/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) => { + setEvento((prev) => (prev ? { ...prev, ...eventoData } : null)); + }, + [] + ); + + const setEventoData = useCallback( + (newEvento: GetEventoWithCuestionariosWithCupos) => { + setEvento(newEvento); + }, + [] + ); + + const value: EventoContextType = { + evento, + loading, + error, + refetch, + updateEvento, + setEventoData, + }; + + return ( + {children} + ); +} + +export function useEvento() { + const context = useContext(EventoContext); + if (context === undefined) { + throw new Error('useEvento must be used within an EventoProvider'); + } + return context; +} diff --git a/src/context/evento/index.ts b/src/context/evento/index.ts new file mode 100644 index 0000000..f8529b2 --- /dev/null +++ b/src/context/evento/index.ts @@ -0,0 +1 @@ +export * from './evento-context'; diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 0000000..1ff9ead --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1 @@ +export * from './evento'; diff --git a/src/styles/sass/bootstrap.scss b/src/styles/sass/bootstrap.scss index 54d4ea2..68e4a35 100644 --- a/src/styles/sass/bootstrap.scss +++ b/src/styles/sass/bootstrap.scss @@ -174,3 +174,7 @@ input { margin-inline: 14rem; } } + +.table-white { + --bs-table-bg: #fff; +} diff --git a/src/types/evento.d.ts b/src/types/evento.d.ts index fa35152..fff1d51 100644 --- a/src/types/evento.d.ts +++ b/src/types/evento.d.ts @@ -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 { diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts new file mode 100644 index 0000000..59af560 --- /dev/null +++ b/src/utils/arrays.ts @@ -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' }, +]; diff --git a/src/utils/date-utils.ts b/src/utils/date-utils.ts index e44ed9f..97b6439 100644 --- a/src/utils/date-utils.ts +++ b/src/utils/date-utils.ts @@ -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}`; +} diff --git a/src/utils/slugify.ts b/src/utils/slugify.ts new file mode 100644 index 0000000..018660c --- /dev/null +++ b/src/utils/slugify.ts @@ -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 }; +}