diff --git a/public/assets/image.png b/public/assets/image.png new file mode 100644 index 0000000..fa6b429 Binary files /dev/null and b/public/assets/image.png differ diff --git a/public/banner-guapa.webp b/public/banner-guapa.webp new file mode 100644 index 0000000..14055f1 Binary files /dev/null and b/public/banner-guapa.webp differ 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]/page.tsx b/src/app/(admins)/administrador/evento/[id_evento]/page.tsx deleted file mode 100644 index 551c312..0000000 --- a/src/app/(admins)/administrador/evento/[id_evento]/page.tsx +++ /dev/null @@ -1,162 +0,0 @@ -'use client'; -import EditEvento from '@/containers/edit-evento'; -import { useGetApi } from '@/hooks/use-get-api'; -import { - CuestionarioWithCupo, - GetEventoWithCuestionarios, -} from '@/types/evento'; -import { useParams } from 'next/navigation'; -import React, { useState, useEffect } from 'react'; -import { CreateEventoType } from '@/types/create-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'; - -type Params = { - id_evento: string; -}; - -export default function Page() { - const params = useParams(); - const { data } = useGetApi( - `/evento/${params.id_evento}/cuestionarios` - ); - - 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 - ); - }; - - const handleEventoActualizado = ( - eventoActualizado: GetEventoWithCuestionarios - ) => { - setEvento(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...

; - - const handleDownload = async (id_cuestionario: number, nombre: string) => { - setLoading(true); - - try { - const res = await axiosInstance.get( - `/cuestionario-respondido/reporte-respuestas/${id_cuestionario}`, - { - responseType: 'blob', - } - ); - - downloadFile(res.data, `${nombre}`, 'csv'); - } catch (error) { - console.error('Error al descargar el archivo:', error); - toast.error('Error al descargar el archivo'); - } finally { - setLoading(false); - } - }; - - return ( -
- - - Volver - - -

Evento

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

Cuestionarios del evento

-
- - - - )} - - ); -} diff --git a/src/app/(admins)/administrador/page.tsx b/src/app/(admins)/administrador/page.tsx deleted file mode 100644 index da32aaf..0000000 --- a/src/app/(admins)/administrador/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client'; - -import Button from '@/components/button'; -import EventoCard from '@/components/evento-card'; -import { useGetApi } from '@/hooks/use-get-api'; -import { GetEventoWithCuestionarios } from '@/types/evento'; -import Link from 'next/link'; -import React from 'react'; - -export default function Page() { - const { loading, data, error } = useGetApi( - '/evento/activos/cuestionarios' - ); - const { data: recientes } = useGetApi( - '/evento/recientes/cuestionarios' - ); - - if (loading) return
Cargando formularios...
; - if (error) return
{error.message}
; - - return ( - <> -
-

Eventos

- - - -
- {loading &&
Cargando...
} - {data && ( -
- {data.map((item, key) => { - const fadeClass = `delay-${(key % 5) + 1}`; - return ( -
- -
- ); - })} -
- )} - {recientes && ( -
-

Eventos Recientes

-

Eventos de los ultimos 30 días.

- {recientes.map((item, key) => { - const fadeClass = `delay-${(key % 5) + 1}`; - return ( -
- -
- ); - })} -
- )} - - ); -} diff --git a/src/app/(admins)/staff/layout.tsx b/src/app/(admins)/staff/layout.tsx deleted file mode 100644 index 2582403..0000000 --- a/src/app/(admins)/staff/layout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Footer from '@/components/layout/footer'; -import Header from '@/components/layout/header'; -import Link from 'next/link'; -import React from 'react'; - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( -
-
-
- - {children} -
-
-
- ); -} diff --git a/src/app/(admins)/staff/lista-manual/page.tsx b/src/app/(admins)/staff/lista-manual/page.tsx deleted file mode 100644 index 994d00c..0000000 --- a/src/app/(admins)/staff/lista-manual/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -'use client'; -import Button from '@/components/button'; -import Input from '@/components/input'; -import Select from '@/components/select'; -import Table, { Header } from '@/components/table'; -import { - GetCuestionario, - GetCuestionarioWithEvento, -} from '@/types/cuestionario'; -import { GetEvento } from '@/types/evento'; -import { ParticipacionEvento } from '@/types/participante-evento'; -import axiosInstance from '@/utils/api-config'; -import React, { useEffect } from 'react'; -import toast from 'react-hot-toast'; - -export default function Page() { - const headers: Header[] = [ - { - key: 'id_participante', - label: '#', - render: (_, row) => row.participante.id_participante, - }, - { - key: 'participante', - label: 'Correo', - render: (_, row) => row.participante.correo, - }, - { - key: 'fecha_registro', - label: 'Fecha registro', - render: (val) => new Date(val as string).toLocaleString(), - }, - { - key: 'estatus', - label: 'Asistencia', - render: (_, row) => { - const isLoading = loadingAsistencia[row.id_participante]; - - return row.fecha_asistencia ? ( - - - Asistió - - ) : ( - - ); - }, - }, - ]; - - const [eventos, setEventos] = React.useState([]); - const [participantes, setParticipantes] = React.useState< - ParticipacionEvento[] - >([]); - const [loadingAsistencia, setLoadingAsistencia] = React.useState< - Record - >({}); - const [busquedaCorreo, setBusquedaCorreo] = React.useState(''); - - useEffect(() => { - const getEventos = async () => { - try { - const { data } = await axiosInstance.get( - `/cuestionario` - ); - if (!data) throw new Error('No se encontraron eventos'); - - const cuestionariosConEvento = await Promise.all( - data.map(async (cuestionario) => { - const { data: evento } = await axiosInstance.get( - `/evento/${cuestionario.id_evento}` - ); - return { ...cuestionario, evento }; - }) - ); - - setEventos(cuestionariosConEvento); - } catch (error) { - console.error('Error en getEventos:', error); - } - }; - - getEventos(); - }, []); - - const handleOnSelect = async (idSeleccionado: number) => { - try { - const { data } = await axiosInstance.get( - `/participante-evento/evento/${idSeleccionado}` - ); - setParticipantes(data); - } catch (error) { - console.error('Error al obtener participante-evento:', error); - } - }; - - const confirmarAsistencia = async ( - id_participante: number, - id_cuestionario: number - ) => { - setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: true })); - try { - await axiosInstance.post( - `/participante-evento/asistencia/${id_participante}/${id_cuestionario}` - ); - - toast.success('Asistencia confirmada'); - // Actualiza lista - await handleOnSelect(id_cuestionario); - } catch (error) { - toast.error('Error al confirmar asistencia'); - console.error(error); - } finally { - setLoadingAsistencia((prev) => ({ ...prev, [id_participante]: false })); - } - }; - - const participantesFiltrados = participantes.filter((p) => - p.participante.correo.toLowerCase().includes(busquedaCorreo.toLowerCase()) - ); - - return ( -
- setBusquedaCorreo(e.target.value)} - /> - -
-
row.id_participante} - /> - - - )} - {participantes.length === 0 && ( -
- No hay participantes registrados para este evento. -
- )} - - ); -} diff --git a/src/app/(admins)/staff/page.tsx b/src/app/(admins)/staff/page.tsx deleted file mode 100644 index 6aa1277..0000000 --- a/src/app/(admins)/staff/page.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import dynamic from 'next/dynamic'; -import { Scanner } from '@yudiel/react-qr-scanner'; -import Button from '@/components/button'; -import axiosInstance from '@/utils/api-config'; -import toast from 'react-hot-toast'; - -const Modal = dynamic(() => import('@/components/modal'), { ssr: false }); - -export default function Page() { - const [scannedData, setScannedData] = useState<{ - id_participante: number; - id_cuestionario: number; - } | null>(null); - - const [showModal, setShowModal] = useState(false); - const [enableScan, setEnableScan] = useState(true); - const [statusMessage, setStatusMessage] = useState(''); - - const [participante, setParticipante] = useState(''); - - const handleScan = async (rawValue: string) => { - console.log('QR Escaneado:', rawValue); - if (!enableScan) return; - - try { - const data = JSON.parse(rawValue); - if (data.id_participante && data.id_cuestionario) { - try { - const response = await axiosInstance.get( - `/participante-evento/${data.id_participante}/${data.id_cuestionario}` - ); - - setParticipante(response.data.participante.correo); - setScannedData({ - id_participante: data.id_participante, - id_cuestionario: data.id_cuestionario, - }); - setShowModal(true); - setEnableScan(false); - } catch (err) { - console.warn('Participante no registrado en el cuestionario:', err); - setStatusMessage( - '❌ El participante no está registrado en este evento.' - ); - } - } else { - setStatusMessage('⚠️ El QR no contiene los campos requeridos'); - } - } catch (err) { - console.error('QR malformado:', err); - setStatusMessage('❌ Error al leer el QR: formato inválido'); - } - }; - - const handleConfirm = async () => { - if (!scannedData) return; - - try { - const response = await axiosInstance.post( - `/participante-evento/asistencia/${scannedData.id_participante}/${scannedData.id_cuestionario}` - ); - - console.log('Asistencia registrada:', response.data); - toast.success('✅ Asistencia registrada correctamente'); - } catch (err) { - console.error('Error en la petición:', err); - setStatusMessage('❌ Error de red al registrar asistencia'); - } - - setShowModal(false); - setEnableScan(true); - setScannedData(null); - }; - - return ( -
-

Lectura de QRs

- - {enableScan ? ( - <> -
- { - if (codes.length > 0) { - handleScan(codes[0].rawValue); - } - }} - onError={(err) => console.error('QR Error:', err)} - constraints={{ facingMode: 'environment' }} - /> -
- - - ) : ( - <> -

La cámara está detenida.

- - - )} - - {statusMessage &&

{statusMessage}

} - - { - setShowModal(false); - setEnableScan(true); - setScannedData(null); - }} - closeButton - className={{ - content: 'p-3', - body: 'text-center', - }} - > -
Datos escaneados
- {scannedData ? ( - <> -

- Participante: {participante} -

- - - ) : ( -

QR inválido

- )} -
-
- ); -} diff --git a/src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx b/src/app/(admins)/user/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)/user/evento/[id_evento]/formulario/[id_cuestionario]/page.tsx index c486950..78f4611 100644 --- a/src/app/(admins)/administrador/evento/[id_evento]/[id_cuestionario]/page.tsx +++ b/src/app/(admins)/user/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,21 +142,23 @@ export default function Page() { p.participante.correo.toLowerCase().includes(busquedaCorreo.toLowerCase()) ); + const breadcrumbItems = [ + { label: 'Inicio', href: '/user/eventos' }, + { + label: evento?.nombre_evento || 'Evento', + href: `/user/evento/${params.id_evento}`, + }, + { label: cuestionario?.nombre_form || 'Formulario' }, + ]; + return (
- - - Volver - - -

Formulario

+ {cuestionario && ( diff --git a/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx b/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx new file mode 100644 index 0000000..f8fafba --- /dev/null +++ b/src/app/(admins)/user/evento/[id_evento]/formularios/crear/page.tsx @@ -0,0 +1,297 @@ +'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 FormularioCardPreview from '@/components/formulario/formulario-card-preview'; +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'; +import { useGetApi } from '@/hooks/use-get-api'; +import { TipoEvento } from '@/types/evento'; + +type Params = { + id_evento: string; +}; + +export default function Page() { + const params = useParams(); + const router = useRouter(); + const { evento, refetch } = useEvento(); + const [datosFormulario, setDatosFormulario] = + useState(null); + const { data: tipo_eventos } = useGetApi(`/tipo-evento`); + + const handleSeleccionarPlantilla = (plantillaId: string) => { + const seleccionada = plantillasDisponibles.find( + (p) => p.id === plantillaId + ); + if (seleccionada && evento) { + // Crear una copia de los datos de la plantilla + const datosPlantilla = { ...seleccionada.datos }; + + // Ajustar las fechas para que estén dentro del rango del evento + const fechaInicioEvento = new Date(evento.fecha_inicio); + const fechaFinEvento = new Date(evento.fecha_fin); + + // Formatear las fechas para inputs datetime-local (YYYY-MM-DDTHH:MM) + const formatoFechaInput = (fecha: Date) => { + return fecha.toISOString().slice(0, 16); + }; + + // Establecer fecha de inicio del formulario igual a la del evento + datosPlantilla.fecha_inicio = formatoFechaInput(fechaInicioEvento); + + // Establecer fecha de fin del formulario igual a la del evento + datosPlantilla.fecha_fin = formatoFechaInput(fechaFinEvento); + + setDatosFormulario(datosPlantilla); + console.log('Plantilla seleccionada:', seleccionada); + console.log('Fechas ajustadas al evento:', { + inicio: datosPlantilla.fecha_inicio, + fin: datosPlantilla.fecha_fin, + }); + } else if (seleccionada) { + // Si no hay evento disponible, usar las fechas originales de la plantilla + setDatosFormulario(seleccionada.datos); + console.log( + 'Plantilla seleccionada (sin ajuste de fechas):', + seleccionada + ); + } + }; + + const breadcrumbItems = [ + { label: 'Inicio', href: '/user/eventos' }, + { + label: evento?.nombre_evento || 'Evento', + href: `/user/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(`/user/evento/${params.id_evento}`); + } catch (error) { + const msg = getAxiosError(error); + toast.error(msg.message || 'Error al crear el formulario'); + } + }; + + return ( +
+ + + {/* Header */} +
+
+
+

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)} + > +
+ +
+
+ ))} +
+
+ )} + + {datosFormulario && ( + <> +
+ {/* Columna Izquierda - Formulario */} +
+
+ {(() => { + const formularioEditor = CreateFormulario({ + formulario: datosFormulario, + onChange: (nuevo) => setDatosFormulario({ ...nuevo }), + evento: evento ? evento : undefined, + tipo_eventos: tipo_eventos?.map((tipo) => ({ + value: tipo.id_tipo_evento, + label: tipo.tipo_evento, + })), + }); + return formularioEditor.informacionBasica(); + })()} + + {/* Botón de crear */} +
+ +
+
+
+ + {/* Columna Derecha - Preview */} +
+
+
+

+ Preview del Formulario +

+
+

Preview del Formulario

+
+ + + {/* Alert de imagen del evento */} + {evento?.banner && ( +
+ + Nota: Al no proporcionar una imagen para + este formulario, se usará automáticamente la imagen del + evento. +
+ )} + +
+ Vista previa: Así se verá tu formulario en + las listas de formularios. +
+
+
+
+
+ + {/* Secciones y preguntas - Ancho completo */} +
+
+ {(() => { + const formularioEditor = CreateFormulario({ + formulario: datosFormulario, + onChange: (nuevo) => setDatosFormulario({ ...nuevo }), + evento: evento ? evento : undefined, + }); + return formularioEditor.seccionesYPreguntas(); + })()} +
+
+ + )} +
+ ); +} diff --git a/src/app/(admins)/user/evento/[id_evento]/layout.tsx b/src/app/(admins)/user/evento/[id_evento]/layout.tsx new file mode 100644 index 0000000..b82638e --- /dev/null +++ b/src/app/(admins)/user/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)/user/evento/[id_evento]/page.tsx b/src/app/(admins)/user/evento/[id_evento]/page.tsx new file mode 100644 index 0000000..744cc67 --- /dev/null +++ b/src/app/(admins)/user/evento/[id_evento]/page.tsx @@ -0,0 +1,132 @@ +'use client'; +import EditEvento from '@/containers/edit-evento'; +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 { downloadFile } from '@/utils/downloas-utils'; +import Breadcrumb from '@/components/breadcrumb'; +import Button from '@/components/button'; +import FormularioCardAdmin from '@/components/formulario/formulario-card-admin'; + +type Params = { + id_evento: string; +}; + +export default function Page() { + const router = useRouter(); + const params = useParams(); + const { evento, loading, error, updateEvento, setEventoData } = useEvento(); + + const [loadingStates, setLoadingStates] = useState>( + {} + ); + + const handleChange = ( + field: keyof CreateEventoType, + value: string | Date + ) => { + // Update the evento state directly using the context + updateEvento({ [field]: value }); + }; + + const handleEventoActualizado = ( + eventoActualizado: GetEventoWithCuestionariosWithCupos + ) => { + // Set the complete updated evento data + setEventoData(eventoActualizado); + }; + + if (loading) return

Cargando...

; + if (error) return

Error: {error}

; + if (!evento) return

No se encontró el evento

; + + const handleDownload = async ( + id_cuestionario: number, + nombre_formulario: string, + nombre_evento: string + ) => { + setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: true })); + + try { + const res = await axiosInstance.get( + `/cuestionario-respondido/reporte-respuestas/${id_cuestionario}`, + { + responseType: 'blob', + } + ); + + downloadFile(res.data, `${nombre_evento} - ${nombre_formulario}`, 'csv'); + } catch (error) { + console.error('Error al descargar el archivo:', error); + toast.error('Error al descargar el archivo'); + } finally { + setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: false })); + } + }; + + return ( +
+ + + + +
+
+
+

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)/user/eventos/crear/page.tsx b/src/app/(admins)/user/eventos/crear/page.tsx new file mode 100644 index 0000000..996811c --- /dev/null +++ b/src/app/(admins)/user/eventos/crear/page.tsx @@ -0,0 +1,128 @@ +'use client'; + +import React, { useState } from 'react'; +import { CreateEventoType } from '@/types/create-evento'; +import CreateEvento from '@/containers/create-evento'; +import EventoCardPreview from '@/components/evento/evento-card-preview'; +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( + `/user/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 ( +
+ {/* Header */} +
+
+
+

Creación del Evento

+

+ Completa la siguiente información para describir los detalles + generales del evento. Presiona siguiente para continuar con la + creación del formulario. +

+
+
+
+ +
+ {/* Columna Izquierda - Formulario */} +
+
+ + + {/* Botón de guardar */} +
+ +
+
+
+ + {/* Columna Derecha - Preview */} +
+
+
+

Preview del Evento

+
+

Preview del Evento

+
+ +
+ Nota: Esta preview es la que veras en tu ruta + de eventos y en la ruta publica de eventos +
+
+
+
+
+
+ ); +} diff --git a/src/app/(admins)/user/eventos/page.tsx b/src/app/(admins)/user/eventos/page.tsx new file mode 100644 index 0000000..06c370d --- /dev/null +++ b/src/app/(admins)/user/eventos/page.tsx @@ -0,0 +1,101 @@ +'use client'; + +import EventoCardAdmin from '@/components/evento/evento-card-admin'; +import FormularioCardAdmin from '@/components/formulario/formulario-card-admin'; +import { useGetApi } from '@/hooks/use-get-api'; +import { GetEventoWithCuestionariosWithCupos } from '@/types/evento'; +import axiosInstance from '@/utils/api-config'; +import { downloadFile } from '@/utils/downloas-utils'; +import React, { useState } from 'react'; +import toast from 'react-hot-toast'; + +export default function Page() { + const [loadingStates, setLoadingStates] = useState>( + {} + ); + const { + loading, + data: activos, + error, + } = useGetApi( + '/evento/activos/cuestionarios' + ); + const { data: recientes } = useGetApi( + '/evento/recientes/cuestionarios' + ); + + if (loading) return
Cargando formularios...
; + if (error) return
{error.message}
; + + const handleDownload = async ( + id_cuestionario: number, + nombre_formulario: string, + nombre_evento: string + ) => { + setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: true })); + + try { + const res = await axiosInstance.get( + `/cuestionario-respondido/reporte-respuestas/${id_cuestionario}`, + { + responseType: 'blob', + } + ); + + downloadFile(res.data, `${nombre_evento} - ${nombre_formulario}`, 'csv'); + } catch (error) { + console.error('Error al descargar el archivo:', error); + toast.error('Error al descargar el archivo'); + } finally { + setLoadingStates((prev) => ({ ...prev, [id_cuestionario]: false })); + } + }; + + return ( +
+ {loading &&
Cargando...
} + {activos && activos.length > 0 && ( +
+

Eventos Activos

+ {activos.map((evento, key) => { + const fadeClass = `delay-${(key % 5) + 1}`; + return ( +
+ +
+ ); + })} +
+ )} + {recientes && recientes.length > 0 && ( +
+

Formularios Recientes

+

Eventos de los ultimos 30 días.

+ {recientes.map((evento) => + evento.cuestionarios.map((cuestionario, cuestionarioIndex) => { + const fadeClass = `delay-${(cuestionarioIndex % 5) + 1}`; + return ( +
+ +
+ ); + }) + )} +
+ )} +
+ ); +} diff --git a/src/app/(admins)/administrador/layout.tsx b/src/app/(admins)/user/layout.tsx similarity index 81% rename from src/app/(admins)/administrador/layout.tsx rename to src/app/(admins)/user/layout.tsx index e98b8ef..239525a 100644 --- a/src/app/(admins)/administrador/layout.tsx +++ b/src/app/(admins)/user/layout.tsx @@ -1,11 +1,13 @@ import Footer from '@/components/layout/footer'; import Header from '@/components/layout/header'; +import Navbar from '@/components/navbar'; import React from 'react'; export default function Layout({ children }: { children: React.ReactNode }) { return (
-
+
+
{children}
diff --git a/src/app/(admins)/administrador/profesores/page.tsx b/src/app/(admins)/user/profesores/page.tsx similarity index 100% rename from src/app/(admins)/administrador/profesores/page.tsx rename to src/app/(admins)/user/profesores/page.tsx diff --git a/src/app/(admins)/user/qr/page.tsx b/src/app/(admins)/user/qr/page.tsx new file mode 100644 index 0000000..d29168d --- /dev/null +++ b/src/app/(admins)/user/qr/page.tsx @@ -0,0 +1,277 @@ +'use client'; + +import React, { useState } from 'react'; +import dynamic from 'next/dynamic'; +import { Scanner } from '@yudiel/react-qr-scanner'; +import Button from '@/components/button'; +import axiosInstance from '@/utils/api-config'; +import toast from 'react-hot-toast'; + +const Modal = dynamic(() => import('@/components/modal'), { ssr: false }); + +export default function Page() { + const [scannedData, setScannedData] = useState<{ + id_participante: number; + id_cuestionario: number; + } | null>(null); + + const [showModal, setShowModal] = useState(false); + const [enableScan, setEnableScan] = useState(true); + const [statusMessage, setStatusMessage] = useState(''); + + const [participante, setParticipante] = useState(''); + + const handleScan = async (rawValue: string) => { + console.log('QR Escaneado:', rawValue); + if (!enableScan) return; + + try { + const data = JSON.parse(rawValue); + if (data.id_participante && data.id_cuestionario) { + try { + const response = await axiosInstance.get( + `/participante-evento/${data.id_participante}/${data.id_cuestionario}` + ); + + setParticipante(response.data.participante.correo); + setScannedData({ + id_participante: data.id_participante, + id_cuestionario: data.id_cuestionario, + }); + setShowModal(true); + setEnableScan(false); + } catch (err) { + console.warn('Participante no registrado en el cuestionario:', err); + setStatusMessage( + '❌ El participante no está registrado en este evento.' + ); + } + } else { + setStatusMessage('⚠️ El QR no contiene los campos requeridos'); + } + } catch (err) { + console.error('QR malformado:', err); + setStatusMessage('❌ Error al leer el QR: formato inválido'); + } + }; + + const handleConfirm = async () => { + if (!scannedData) return; + + try { + const response = await axiosInstance.post( + `/participante-evento/asistencia/${scannedData.id_participante}/${scannedData.id_cuestionario}` + ); + + console.log('Asistencia registrada:', response.data); + toast.success('✅ Asistencia registrada correctamente'); + } catch (err) { + console.error('Error en la petición:', err); + setStatusMessage('❌ Error de red al registrar asistencia'); + } + + setShowModal(false); + setEnableScan(true); + setScannedData(null); + }; + + return ( +
+
+
+
+
+
+ +
+

Registro de Asistencia

+

+ Escanea el código QR para registrar la asistencia +

+
+ +
+ {enableScan ? ( +
+
+
+ { + if (codes.length > 0) { + handleScan(codes[0].rawValue); + } + }} + onError={(err) => console.error('QR Error:', err)} + constraints={{ facingMode: 'environment' }} + styles={{ + container: { + width: '100%', + maxWidth: '300px', + borderRadius: '8px', + overflow: 'hidden', + }, + }} + /> +
+
+ +
+
+ + + Mantén el código QR centrado en el marco de la cámara + +
+
+ + +
+ ) : ( +
+
+ +
+

Cámara desactivada

+

+ La cámara está detenida. Actívala para continuar escaneando + códigos QR. +

+ +
+ )} + + {statusMessage && ( +
+
+
+ {statusMessage.includes('❌') && ( + + )} + {statusMessage.includes('⚠️') && ( + + )} + {!statusMessage.includes('❌') && + !statusMessage.includes('⚠️') && ( + + )} +
+ {statusMessage} +
+
+ )} +
+
+
+
+ + { + setShowModal(false); + setEnableScan(true); + setScannedData(null); + }} + closeButton + className={{ + content: 'border-0 rounded-3', + body: 'text-center', + }} + > +
+
+ + QR Escaneado Correctamente +
+
+ +
+ {scannedData ? ( + <> +
+
+ +
+
+ Información del participante +
+
+
+
+ + Correo: + + {participante} + +
+
+
+
+ +
+ + +
+ + ) : ( +
+
+ +
+
QR inválido o malformado
+
+ )} +
+
+
+ ); +} diff --git a/src/app/(admins)/user/usuarios/page.tsx b/src/app/(admins)/user/usuarios/page.tsx new file mode 100644 index 0000000..540c060 --- /dev/null +++ b/src/app/(admins)/user/usuarios/page.tsx @@ -0,0 +1,45 @@ +'use client'; +import Button from '@/components/button'; +import Table, { Header } from '@/components/table'; +import { useGetApi } from '@/hooks/use-get-api'; +import { Administrador } from '@/types/administradores'; +import React from 'react'; + +export default function Page() { + const { data } = useGetApi('/administrador'); + + const headers: Header[] = [ + { key: 'id_administrador', label: 'ID' }, + { key: 'nombre_usuario', label: 'Nombre de Usuario' }, + { key: 'correo', label: 'Correo' }, + { + key: 'tipoUser', + render: (_, row) => row.tipoUser.tipo, + label: 'Tipo de Usuario', + }, + { + key: 'tipoUser', + render: () => ( +
+ + +
+ ), + label: 'Acciones', + }, + ]; + + return ( +
+
+

Administradores

+ +
+ {data &&
} + + ); +} diff --git a/src/app/(auth)/login/administradores/page.tsx b/src/app/(auth)/login/administradores/page.tsx deleted file mode 100644 index 7b582df..0000000 --- a/src/app/(auth)/login/administradores/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client'; -import Button from '@/components/button'; -import Input from '@/components/input'; -import { useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import Cookies from 'js-cookie'; - -export default function Page() { - const router = useRouter(); - const [formData, setFormData] = useState({ username: '', password: '' }); - const [error, setError] = useState(''); - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, - }); - setError(''); - }; - - const handleOnSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const { username, password } = formData; - - if (username === 'user-admin' && password === '@dm1nP@ss') { - Cookies.set('token', 'staff1'); - router.push('/administrador'); - } else { - setError('Usuario o contraseña incorrectos'); - } - }; - - return ( -
-

Iniciar sesión

-

Administradores

- -
- - - {error && ( -
- {error} -
- )} -
- -
- -
- ); -} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..aa355b9 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,166 @@ +'use client'; +import SimpleInput from '@/components/input'; +import PasswordInput from '@/components/password'; +import React from 'react'; +import { useRouter } from 'next/navigation'; +import axiosInstance from '@/utils/api-config'; + +export default function LoginForm() { + const router = useRouter(); + + const [loginData, setLoginData] = React.useState({ + nombre_usuario: '', + password: '', + }); + + const [error, setError] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setLoginData((prevData) => ({ ...prevData, [name]: value })); + // Clear error when user starts typing + if (error) setError(''); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(''); + + try { + const response = await axiosInstance.post('/administrador/login', { + nombre_usuario: loginData.nombre_usuario, + password: loginData.password, + }); + + // Guardar el token en sessionStorage + if (response.data.token) { + sessionStorage.setItem('token', response.data.token); + + // Opcional: guardar información adicional del usuario si viene en la respuesta + if (response.data.user) { + sessionStorage.setItem('user', JSON.stringify(response.data.user)); + } + + // Redireccionar al dashboard + router.push('/user/eventos'); + } else { + setError('No se recibió token de autenticación'); + } + } catch (error: unknown) { + console.error('Error en login:', error); + + // Type guard para axios error + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as { + response?: { status?: number; data?: { message?: string } }; + }; + + if (axiosError.response?.status === 401) { + setError('Credenciales incorrectas'); + } else if (axiosError.response?.data?.message) { + setError(axiosError.response.data.message); + } else { + setError('Error al iniciar sesión. Inténtalo de nuevo.'); + } + } else { + setError('Error al iniciar sesión. Inténtalo de nuevo.'); + } + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+
+
+
+
+

+ Iniciar sesión +

+ +
+ Bienvenido de vuelta al{' '} + Sistema de Eventos +
+

+ Ingresa tu correo electrónico y tu contraseña para acceder + a tu cuenta. +

+ + {/* Error message */} + {error && ( +
{error}
+ )} + +
+ + + +
+ +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/app/(auth)/login/staff/page.tsx b/src/app/(auth)/login/staff/page.tsx deleted file mode 100644 index ee513ef..0000000 --- a/src/app/(auth)/login/staff/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; -import Button from '@/components/button'; -import Input from '@/components/input'; -import { useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import Cookies from 'js-cookie'; - -export default function Page() { - const router = useRouter(); - const [formData, setFormData] = useState({ username: '', password: '' }); - const [error, setError] = useState(''); - - const handleChange = (e: React.ChangeEvent) => { - setFormData({ - ...formData, - [e.target.name]: e.target.value, - }); - setError(''); - }; - - const handleOnSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const { username, password } = formData; - - if (username === 'staff1' && password === 'st@ffP@ss') { - Cookies.set('token', 'staff1'); - router.push('/staff'); - } else { - setError('Usuario o contraseña incorrectos'); - } - }; - - return ( -
-

Iniciar sesión

-

Staff

- -
- - - {error &&
{error}
} -
- -
- -
- ); -} 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 444e2ac..8d164cf 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -1,37 +1,84 @@ 'use client'; import ClientCarousel from '@/client-components/client-carousel'; -import EventoCard from '@/components/evento-card'; +import FormularioCardUser from '@/components/formulario/formulario-card-user'; +import EmptyEventsState from '@/components/empty-events-state'; import { useGetApi } from '@/hooks/use-get-api'; -import { GetEventoWithCuestionarios } from '@/types/evento'; +import { GetEventoWithCuestionariosWithCupos } from '@/types/evento'; import React from 'react'; export default function Page() { - const { loading, data } = useGetApi( + const { loading, data } = useGetApi( '/evento/activos/cuestionarios' ); return (
-
- -
- {loading &&
Cargando...
} - {data && ( + {data && data.length > 0 && ( +
+ 0 + ? data.map((evento) => ({ + src: `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}`, + alt: `Banner de ${evento.nombre_evento}`, + })) + : [ + { + src: '/default-banner.jpeg', + alt: 'Banner de eventos activos', + }, + ] + } + /> +
+ )} + + {loading && ( +
+
+
+ Cargando... +
+
+
Cargando eventos...
+ + Esto solo tomará unos segundos + +
+
+
+ )} + + {!loading && data && data.length === 0 && ( +
+ +
+ )} + + {!loading && data && data.length > 0 && (
- {data.map((item, key) => { - const fadeClass = `delay-${(key % 5) + 1}`; - return ( -
- -
- ); - })} + {data.flatMap((evento) => + evento.cuestionarios.map((cuestionario, index) => { + return ( +
+ +
+ ); + }) + )}
)}
diff --git a/src/components/banner-uploader.tsx b/src/components/banner-uploader.tsx index 32aa635..96bd186 100644 --- a/src/components/banner-uploader.tsx +++ b/src/components/banner-uploader.tsx @@ -8,12 +8,16 @@ interface BannerUploaderProps { label?: string; onChange?: (file: File | null) => void; defaultBanner?: string; + className?: string; + showPreview?: boolean; } export default function ImageUploader({ label = 'Subir imagen', onChange, defaultBanner, + className = '', + showPreview = true, }: BannerUploaderProps) { const fileInputRef = useRef(null); const [preview, setPreview] = useState(defaultBanner || null); @@ -41,17 +45,28 @@ export default function ImageUploader({ }; return ( -
+
{label && } - - {preview && ( -
+
+ + {preview && ( + + )} +
+ {preview && showPreview && ( +
-
)}
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 new file mode 100644 index 0000000..d615ab4 --- /dev/null +++ b/src/components/cuestionario-card.tsx @@ -0,0 +1,106 @@ +'use client'; +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 CuestionarioCard({ + 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}
+ +
+
+ +
+
+ {evento.cuestionarios[0]?.cupo_maximo === null ? ( + Sin límite + ) : ( + + {evento.cuestionarios[0]?.cupos_disponibles} cupos disponibles + + )} + + {descripcion.length > limite && ( + + )} +
+ + {evento.cuestionarios.length > 0 && + evento.cuestionarios.map((cuestionario) => ( +
+ {user === 'administrador' || user === 'staff' ? ( + + Ver evento + + ) : cuestionario.cupos_disponibles === 0 && + cuestionario.cupo_maximo !== null ? ( + + ) : ( + + Registro + + )} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/empty-events-state.tsx b/src/components/empty-events-state.tsx new file mode 100644 index 0000000..b285531 --- /dev/null +++ b/src/components/empty-events-state.tsx @@ -0,0 +1,62 @@ +'use client'; + +import React from 'react'; +import Button from './button'; + +export default function EmptyEventsState() { + return ( +
+
+
+
+ {/* Ilustración principal con Bootstrap */} +
+
+ +
+
+ + {/* Título y mensaje principal */} +
+

+ No hay eventos disponibles +

+

+ En este momento no tenemos eventos activos para mostrar. +
+ + ¡No te preocupes! + {' '} + Pronto habrá nuevas oportunidades de participación. +

+
+ + {/* Botón de acción usando clases Bootstrap */} +
+ +
+ + {/* Mensaje adicional */} +
+ + + Esta página se actualiza automáticamente para mostrar los + eventos más recientes + +
+
+
+
+
+ ); +} diff --git a/src/components/evento-card.tsx b/src/components/evento-card.tsx index 6ea130d..595124e 100644 --- a/src/components/evento-card.tsx +++ b/src/components/evento-card.tsx @@ -1,15 +1,15 @@ -'use client'; -import { GetEventoWithCuestionarios } from '@/types/evento'; +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: GetEventoWithCuestionarios; + evento: GetEventoWithCuestionariosWithCupos; user?: 'public' | 'administrador' | 'staff'; }) { const [verMas, setVerMas] = useState(false); @@ -43,18 +43,9 @@ export default function EventoCard({ />
{evento.nombre_evento}
-
-
- {evento.cuestionarios[0]?.cupo_maximo === null ? ( - Sin límite - ) : ( - - {evento.cuestionarios[0]?.cupos_disponibles} cupos disponibles - - )} {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)} + - {evento.cuestionarios.length > 0 && - evento.cuestionarios.map((cuestionario) => ( -
- {user === 'administrador' || user === 'staff' ? ( - - Ver evento - - ) : cuestionario.cupos_disponibles === 0 && - cuestionario.cupo_maximo !== null ? ( - - ) : ( - - Registro - - )} -
- ))} + {/* 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..a6587a2 --- /dev/null +++ b/src/components/evento/evento-card-admin.tsx @@ -0,0 +1,153 @@ +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 { formatearFechaCard } from '@/utils/date-utils'; + +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; + + // Obtener información de fecha usando la utilidad + const { mesTexto, diaTexto } = formatearFechaCard( + evento.fecha_inicio, + evento.fecha_fin + ); + + 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 */} +
+
+
+ {mesTexto} +
+
2 ? '1rem' : '1.25rem', + }} + > + {diaTexto} +
+
+
+
{evento.nombre_evento}
+
+
+ + {/* Descripción */} +
+ + {descripcion.length > limite && ( + + )} +
+ + {/* Formularios/sub eventos */} + {evento.cuestionarios.length > 0 ? ( +
+
+ Formularios disponibles: +
+
+ {evento.cuestionarios.slice(0, 3).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-preview.tsx b/src/components/evento/evento-card-preview.tsx new file mode 100644 index 0000000..bbb6651 --- /dev/null +++ b/src/components/evento/evento-card-preview.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { CreateEventoType } from '@/types/create-evento'; +import Image from 'next/image'; +import React, { useState, useEffect } from 'react'; +import MarkdownRenderer from '@/components/markdown-render'; +import { formatearFechaCard } from '@/utils/date-utils'; +import Button from '../button'; + +interface EventoPreviewProps { + evento: CreateEventoType; + eventoBanner?: File | null; + existingBanner?: string | null; // Banner existente desde la API + cuestionariosCount?: number; // Número de formularios existentes +} + +export default function EventoCardPreview({ + evento, + eventoBanner, + existingBanner, + cuestionariosCount = 0, +}: EventoPreviewProps) { + const [verMas, setVerMas] = useState(false); + const [bannerUrl, setBannerUrl] = useState('/default-banner.png'); + + const descripcion = evento.descripcion_evento ?? ''; + const limite = 100; + + const descripcionRecortada = + descripcion.length > limite && !verMas + ? `${descripcion.slice(0, limite)}...` + : descripcion; + + // Actualizar URL del banner cuando cambie el archivo o cuando haya un banner existente + useEffect(() => { + if (eventoBanner) { + // Si hay un nuevo banner seleccionado, usarlo + const url = URL.createObjectURL(eventoBanner); + setBannerUrl(url); + + // Limpiar URL anterior cuando el componente se desmonte + return () => URL.revokeObjectURL(url); + } else if (existingBanner) { + // Si no hay nuevo banner pero sí hay uno existente desde la API, usarlo + setBannerUrl( + `${process.env.NEXT_PUBLIC_API_URL}/banners/${existingBanner}` + ); + } else { + // Si no hay ningún banner, usar el banner por defecto + setBannerUrl('/default-banner.png'); + } + }, [eventoBanner, existingBanner]); + + // Obtener información de fecha usando la utilidad + const getDateInfo = () => { + if (!evento.fecha_inicio || !evento.fecha_fin) { + return { mesTexto: 'MES', diaTexto: 'DD' }; + } + + return formatearFechaCard(evento.fecha_inicio, evento.fecha_fin); + }; + + const { mesTexto, diaTexto } = getDateInfo(); + + return ( +
+
+ Banner formulario + + {/* Badge de cantidad de formularios en la esquina superior derecha */} +
+ {cuestionariosCount > 0 ? ( + + {cuestionariosCount} formulario + {cuestionariosCount !== 1 ? 's' : ''} + + ) : ( + Sin formularios + )} +
+
+ +
+ {/* Fecha del evento */} +
+
+
+ {mesTexto} +
+
2 ? '1rem' : '1.25rem', + }} + > + {diaTexto} +
+
+
+
+ {evento.nombre_evento || 'Nombre del evento'} +
+
+
+ + {/* Tipo de evento */} + {evento.tipo_evento && ( +
+ + + {evento.tipo_evento} + +
+ )} + + {/* Descripción */} +
+ {descripcion ? ( + <> + + {descripcion.length > limite && ( + + )} + + ) : ( +

Descripción del evento...

+ )} +
+ + {/* Botón principal - deshabilitado */} + +
+
+ ); +} 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..9321db0 --- /dev/null +++ b/src/components/formulario/formulario-card-admin.tsx @@ -0,0 +1,181 @@ +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 { formatearFechaCard } from '@/utils/date-utils'; +import Button from '../button'; + +export default function FormularioCardAdmin({ + cuestionario, + evento, + handleDownload, + disabled, +}: { + cuestionario: CuestionarioWithCupo; + evento: GetEvento; + handleDownload: ( + id_cuestionario: number, + nombre_formulario: string, + nombre_evento: string + ) => void; + disabled: boolean; +}) { + const [verMas, setVerMas] = useState(false); + const descripcion = cuestionario.descripcion ?? ''; + const limite = 100; + + const descripcionRecortada = + descripcion.length > limite && !verMas + ? `${descripcion.slice(0, limite)}...` + : descripcion; + + // Obtener información de fecha usando la utilidad + const { mesTexto, diaTexto } = formatearFechaCard( + cuestionario.fecha_inicio, + cuestionario.fecha_fin + ); + + return ( +
+
+ + Banner formulario + + + {/* Mensaje cuando se usa la imagen del evento */} + {!cuestionario.banner && evento.banner && ( +
+
+ + Usando imagen del evento +
+
+ )} + + {/* Badge de tipo de cuestionario en la esquina superior izquierda */} +
+ + {cuestionario.tipoCuestionario?.tipo_cuestionario || 'Formulario'} + +
+
+ +
+ {/* Fecha del formulario */} +
+
+
+ {mesTexto} +
+
2 ? '1rem' : '1.25rem', + }} + > + {diaTexto} +
+
+
+
+ {cuestionario.nombre_form} +
+ {evento && ( + + + {evento.nombre_evento} + + )} +
+
+ + {/* Descripción */} +
+ + {descripcion.length > limite && ( + + )} +
+ + {/* Información de cupos y estadísticas */} +
+
+
+ +
+ Cupos + + {cuestionario.cupo_maximo !== null + ? `${cuestionario.cupos_usados} / ${cuestionario.cupo_maximo}` + : 'Sin límite'} + +
+
+
+
+ + {/* Botones de acción */} +
+ + Ver/Editar formulario + + + {/* Botón secundario para ver respuestas */} + +
+
+
+ ); +} diff --git a/src/components/formulario/formulario-card-create-preview.tsx b/src/components/formulario/formulario-card-create-preview.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/formulario/formulario-card-preview-unified.tsx b/src/components/formulario/formulario-card-preview-unified.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/formulario/formulario-card-preview.tsx b/src/components/formulario/formulario-card-preview.tsx new file mode 100644 index 0000000..aa3fc6c --- /dev/null +++ b/src/components/formulario/formulario-card-preview.tsx @@ -0,0 +1,280 @@ +import { GetCuestionario } from '@/types/cuestionario'; +import { FormularioCreacion } from '@/types/create-formulario'; +import { GetEvento, GetEventoWithCuestionariosWithCupos } from '@/types/evento'; +import Image from 'next/image'; +import React, { useState, useEffect } from 'react'; +import MarkdownRenderer from '../markdown-render'; +import { formatearFechaCard } from '@/utils/date-utils'; + +interface FormularioCardPreviewProps { + // Para formularios existentes + cuestionario?: GetCuestionario; + // Para formularios en creación + formulario?: FormularioCreacion; + // Evento asociado + evento?: GetEvento | GetEventoWithCuestionariosWithCupos | null; + // Banner personalizado + nuevoBanner?: File | null; +} + +export default function FormularioCardPreview({ + cuestionario, + formulario, + evento, + nuevoBanner, +}: FormularioCardPreviewProps) { + const [verMas, setVerMas] = useState(false); + const [bannerUrl, setBannerUrl] = useState('/default-banner.png'); + + // Determinar qué datos usar (formulario existente o en creación) + const isCreating = !!formulario && !cuestionario; + const data = cuestionario || formulario; + + // Manejar la imagen del banner + useEffect(() => { + if (nuevoBanner) { + // Si hay un nuevo banner seleccionado, usarlo + const url = URL.createObjectURL(nuevoBanner); + setBannerUrl(url); + return () => URL.revokeObjectURL(url); + } else if (!isCreating && cuestionario?.banner) { + // Si es un formulario existente y tiene banner propio + setBannerUrl( + `${process.env.NEXT_PUBLIC_API_URL}/banners/${cuestionario.banner}` + ); + } else if (evento?.banner) { + // Si hay banner del evento, usarlo + setBannerUrl( + `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}` + ); + } else { + // Banner por defecto + setBannerUrl('/default-banner.png'); + } + }, [nuevoBanner, evento?.banner, cuestionario?.banner, isCreating]); + + if (!data) { + return null; // No hay datos para mostrar + } + + const descripcion = data.descripcion ?? ''; + const limite = 100; + + const descripcionRecortada = + descripcion.length > limite && !verMas + ? `${descripcion.slice(0, limite)}...` + : descripcion; + + // Obtener información de fecha usando la utilidad + const getDateInfo = () => { + if (!data.fecha_inicio || !data.fecha_fin) { + return { mesTexto: 'MES', diaTexto: 'DD' }; + } + + let fechaInicio: Date, fechaFin: Date; + + if (isCreating) { + // Para formularios en creación, las fechas vienen como string + fechaInicio = new Date(data.fecha_inicio); + fechaFin = new Date(data.fecha_fin); + } else { + // Para formularios existentes, convertir a Date también + fechaInicio = new Date(data.fecha_inicio); + fechaFin = new Date(data.fecha_fin); + } + + return formatearFechaCard(fechaInicio, fechaFin); + }; + + const { mesTexto, diaTexto } = getDateInfo(); + + // Determinar si se está usando la imagen del evento + const isUsingEventImage = + !nuevoBanner && (isCreating || !cuestionario?.banner) && evento?.banner; + + // Obtener el tipo de cuestionario + const getTipoCuestionario = () => { + const tipoId = isCreating + ? (formulario as FormularioCreacion).id_tipo_cuestionario + : (cuestionario as GetCuestionario).id_tipo_cuestionario; + + if (tipoId === 1) return 'Registro'; + if (tipoId === 2) return 'Evaluación'; + return 'Formulario'; + }; + + // Obtener nombre del formulario + const getNombreFormulario = () => { + return data.nombre_form || 'Nombre del formulario'; + }; + + // Obtener información de cupos + const getCuposInfo = () => { + if (isCreating) { + const form = formulario as FormularioCreacion; + return form.cupo_maximo && form.cupo_maximo > 0 + ? `0 / ${form.cupo_maximo}` + : 'Sin límite'; + } else { + const cuest = cuestionario as GetCuestionario; + return cuest.cupo_maximo !== null && cuest.cupo_maximo !== undefined + ? `0 / ${cuest.cupo_maximo}` + : 'Sin límite'; + } + }; + + // Obtener información de secciones y preguntas (solo para formularios en creación) + const getSeccionesInfo = () => { + if (!isCreating || !formulario) return null; + + const totalSecciones = formulario.secciones.length; + const totalPreguntas = formulario.secciones.reduce( + (total, seccion) => total + seccion.preguntas.length, + 0 + ); + + return { totalSecciones, totalPreguntas }; + }; + + const seccionesInfo = getSeccionesInfo(); + + return ( +
+
+ Banner formulario + + {/* Mensaje cuando se usa la imagen del evento */} + {isUsingEventImage && ( +
+
+ + Usando imagen del evento +
+
+ )} + + {/* Badge de tipo de cuestionario en la esquina superior izquierda */} +
+ {getTipoCuestionario()} +
+
+ +
+ {/* Fecha del formulario */} +
+
+
+ {mesTexto} +
+
2 ? '1rem' : '1.25rem', + }} + > + {diaTexto} +
+
+
+
{getNombreFormulario()}
+ {evento && ( + + + {evento.nombre_evento} + + )} +
+
+ + {/* Descripción */} +
+ {descripcion ? ( + <> + + {descripcion.length > limite && ( + + )} + + ) : ( +

Descripción del formulario...

+ )} +
+ + {/* Información de cupos */} +
+
+
+ +
+ Cupos + {getCuposInfo()} +
+
+
+
+ + {/* Información de secciones y preguntas (solo para formularios en creación) */} + {seccionesInfo && ( +
+
+
+
+ + + {seccionesInfo.totalSecciones} sección + {seccionesInfo.totalSecciones !== 1 ? 'es' : ''} + +
+
+ + + {seccionesInfo.totalPreguntas} pregunta + {seccionesInfo.totalPreguntas !== 1 ? 's' : ''} + +
+
+
+
+ )} + + {/* Botón de acción */} +
+ + +
+
+
+ ); +} diff --git a/src/components/formulario/formulario-card-user.tsx b/src/components/formulario/formulario-card-user.tsx new file mode 100644 index 0000000..c3fd5ab --- /dev/null +++ b/src/components/formulario/formulario-card-user.tsx @@ -0,0 +1,211 @@ +'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'; +import { formatearFechaCard, formatearHorario } from '@/utils/date-utils'; +import Button from '../button'; + +export default function FormularioCardUser({ + evento, + formulario, +}: { + evento: GetEvento; + formulario: CuestionarioWithCupo; +}) { + const [verMas, setVerMas] = useState(false); + const descripcion = formulario.descripcion ?? ''; + const limite = 100; + + // Determinar si hay cupos disponibles + const sinCupos = + formulario.cupo_maximo !== null && formulario.cupos_disponibles <= 0; + + // Verificar si el evento es el mismo día + const fechaInicio = new Date(formulario.fecha_inicio); + const fechaFin = new Date(formulario.fecha_fin); + const esElMismoDia = fechaInicio.toDateString() === fechaFin.toDateString(); + + // Obtener información de fecha formateada usando las utilidades + const { mesTexto, diaTexto } = formatearFechaCard( + formulario.fecha_inicio, + formulario.fecha_fin + ); + + // Obtener horario formateado si es el mismo día + const horarioFormateado = esElMismoDia + ? formatearHorario(formulario.fecha_inicio, formulario.fecha_fin) + : null; + + const descripcionRecortada = + descripcion.length > limite && !verMas + ? `${descripcion.slice(0, limite)}...` + : descripcion; + + const imgSrc = formulario.banner + ? `${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}` + : evento.banner + ? `${process.env.NEXT_PUBLIC_API_URL}/banners/${evento.banner}` + : `/default-banner.png`; + + return ( +
+
+ {sinCupos ? ( +
+ Banner formulario +
+ Sin cupos disponibles +
+
+ ) : ( + + Banner formulario + + )} + + {/* Badge de cupos en la esquina superior derecha */} +
+ + {formulario.tipoCuestionario.tipo_cuestionario}{' '} + +
+
+ +
+ {/* Fecha del evento */} +
+
+
{mesTexto}
+
+ {diaTexto} +
+
+
+ {evento.nombre_evento} +
+ {formulario.nombre_form} +
+ {formulario.tipoEvento && ( +
+ {formulario.tipoEvento.tipo_evento} +
+ )} +
+
+ + {/* Descripción */} +
+ + {descripcion.length > limite && ( + + )} +
+ + {/* Información de cupos y estadísticas */} +
+ {/* Columna de horario - solo visible si es el mismo día */} + {esElMismoDia && ( +
+
+
+ +
+ Horario + + {horarioFormateado} + +
+
+
+
+ )} + {/* Columna de cupos - siempre visible */} +
+
+
+ +
+ Cupos + + {formulario.cupo_maximo !== null + ? `${formulario.cupos_usados} / ${formulario.cupo_maximo}` + : 'Sin límite'} + + {sinCupos && ( +
+ + Cupos agotados +
+ )} +
+
+
+
+
+ + {/* Botón de registro */} + {sinCupos ? ( + + ) : ( + + Registro + + )} +
+
+ ); +} diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index 18c9c3f..09577cb 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,9 +1,22 @@ -import React from "react"; -import Link from "next/link"; +'use client'; +import React from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; const Navbar: React.FC = () => { + const pathname = usePathname(); + + const navItems = [ + { href: '/', label: 'Inicio' }, + { href: '/user/eventos', label: 'Eventos' }, + { href: '/user/qr', label: 'Lector de QRs' }, + { href: '/user/eventos/crear', label: 'Crear Evento' }, + { href: '/user/usuarios', label: 'Usuarios' }, + { href: '/signout', label: 'Cerrar sesión' }, + ]; + return ( -