feat: Implement event and questionnaire management features

- Added event creation page with form handling and validation.
- Introduced layout for event management with context provider.
- Created event listing page with active and recent events display.
- Developed questionnaire creation page linked to events.
- Implemented breadcrumb navigation for better user experience.
- Added reusable components for event and questionnaire cards.
- Integrated API calls for fetching and managing event data.
- Enhanced slugify utility for better URL handling.
- Established context for managing event state across components.
This commit is contained in:
miguel
2025-08-12 20:00:21 -06:00
parent 5b0f0ad6bf
commit be8f207ecd
35 changed files with 2091 additions and 642 deletions
@@ -0,0 +1,121 @@
'use client';
import { CuestionarioWithCupo, GetEvento } from '@/types/evento';
import Image from 'next/image';
import Link from 'next/link';
import React, { useState } from 'react';
import MarkdownRenderer from '../markdown-render';
import { toParam } from '@/utils/slugify';
export default function FormularioCardUser({
evento,
formulario,
}: {
evento: GetEvento;
formulario: CuestionarioWithCupo;
}) {
const [verMas, setVerMas] = useState(false);
const descripcion = formulario.descripcion ?? '';
const limite = 100;
const descripcionRecortada =
descripcion.length > limite && !verMas
? `${descripcion.slice(0, limite)}...`
: descripcion;
return (
<div
className="card shadow-sm border-0"
style={{ borderRadius: '12px', overflow: 'hidden' }}
>
<div className="position-relative">
<Link
href={`/evento/${toParam(
evento.nombre_evento,
evento.id_evento
)}/${toParam(formulario.nombre_form, formulario.id_cuestionario)}`}
>
<Image
width={400}
height={250}
className="img-fluid w-100"
src={`${process.env.NEXT_PUBLIC_API_URL}/banners/${formulario.banner}`}
alt="Banner formulario"
style={{ objectFit: 'cover', height: '200px' }}
/>
</Link>
{/* Badge de cupos en la esquina superior derecha */}
<div className="position-absolute top-0 end-0 m-2">
{formulario.cupo_maximo === null ? (
<span className="badge bg-success">Sin límite</span>
) : (
<span className="badge bg-primary">
{formulario.cupos_disponibles} cupos
</span>
)}
</div>
</div>
<div className="card-body p-3 d-flex flex-column">
{/* Fecha del evento */}
<div className="d-flex align-items-center mb-2">
<div className="me-3">
<div className="text-muted text-center small">
{new Date(formulario.fecha_inicio)
.toLocaleDateString('es-ES', { month: 'short' })
.toUpperCase()}
</div>
<div
className="fw-bold h4 mb-0"
style={{ lineHeight: 1, whiteSpace: 'nowrap' }}
>
{(() => {
const fechaInicio = new Date(formulario.fecha_inicio);
const fechaFin = new Date(formulario.fecha_fin);
const diaInicio = fechaInicio.getDate();
const diaFin = fechaFin.getDate();
const esElMismoDia =
fechaInicio.toDateString() === fechaFin.toDateString();
return esElMismoDia ? diaInicio : `${diaInicio} - ${diaFin}`;
})()}
</div>
</div>
<div>
<span className="small text-muted fw-bold">
{evento.nombre_evento}
</span>
<h5 className="card-title mb-1 fw-bold">
{formulario.nombre_form}
</h5>
</div>
</div>
{/* Descripción */}
<div className="card-description flex-grow-1 mb-3">
<MarkdownRenderer markdown={descripcionRecortada} />
{descripcion.length > limite && (
<button
onClick={() => setVerMas(!verMas)}
className="btn btn-link btn-sm p-0 ms-1 align-baseline text-decoration-none"
style={{ fontSize: '0.875rem' }}
>
{verMas ? 'Ver menos' : 'Ver más'}
</button>
)}
</div>
{/* Botón de registro */}
<Link
href={`/evento/${toParam(
evento.nombre_evento,
evento.id_evento
)}/${toParam(formulario.nombre_form, formulario.id_cuestionario)}`}
className="btn btn-primary w-100 rounded-pill"
>
Registro
</Link>
</div>
</div>
);
}