Fix, all good, hola

This commit is contained in:
jorgemike
2025-04-03 21:05:11 -06:00
parent 8b3d695d1d
commit 2aac2488fd
26 changed files with 886 additions and 595 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"printWidth": 80,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
+102 -9
View File
@@ -8,13 +8,14 @@
"name": "formularios_front",
"version": "0.1.0",
"dependencies": {
"@yudiel/react-qr-scanner": "^2.2.1",
"axios": "^1.8.4",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"html5-qrcode": "^2.3.8",
"next": "15.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-hot-toast": "^2.5.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -1215,6 +1216,12 @@
"@popperjs/core": "^2.9.2"
}
},
"node_modules/@types/emscripten": {
"version": "1.40.1",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.40.1.tgz",
"integrity": "sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -1715,6 +1722,20 @@
"win32"
]
},
"node_modules/@yudiel/react-qr-scanner": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-2.2.1.tgz",
"integrity": "sha512-5tmzoi9d8mqqaxKTxfI9kulS3N3Kph75AOqwU2lnl4IMwjmLcTXS8IyecDUvzFNuoMNhCpofrqqGueXm8IXkjA==",
"license": "MIT",
"dependencies": {
"barcode-detector": "^3.0.0",
"webrtc-adapter": "9.0.1"
},
"peerDependencies": {
"react": "^17 || ^18 || ^19",
"react-dom": "^17 || ^18 || ^19"
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -2023,6 +2044,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/barcode-detector": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-3.0.1.tgz",
"integrity": "sha512-3fCzG/Py4SVgZJhubD1mt7rVprtHEVWrxQN4FUOG0oulPE4193evbgyptxcOYsfTNEtMlWc+Ec9tdxhjlR4/Ww==",
"license": "MIT",
"dependencies": {
"zxing-wasm": "^2.1.0"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
@@ -2294,7 +2324,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -3408,6 +3437,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
"integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -3518,12 +3556,6 @@
"node": ">= 0.4"
}
},
"node_modules/html5-qrcode": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz",
"integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==",
"license": "Apache-2.0"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4699,6 +4731,23 @@
"react": "^19.1.0"
}
},
"node_modules/react-hot-toast": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
"integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -4922,6 +4971,12 @@
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT"
},
"node_modules/sdp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -5445,6 +5500,18 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "4.39.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz",
"integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -5600,6 +5667,19 @@
"punycode": "^2.1.0"
}
},
"node_modules/webrtc-adapter": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz",
"integrity": "sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==",
"license": "BSD-3-Clause",
"dependencies": {
"sdp": "^3.2.0"
},
"engines": {
"node": ">=6.0.0",
"npm": ">=3.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5727,6 +5807,19 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zxing-wasm": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-2.1.0.tgz",
"integrity": "sha512-CvuwDZHRHwg6PeCARaCDIp3dauD4cin0mbHrQQZtMDrr5mblPzCimAjdw/XMhD/Au10q/f5+SAupvYqYvUOg1Q==",
"license": "MIT",
"dependencies": {
"@types/emscripten": "^1.40.0",
"type-fest": "^4.35.0"
},
"peerDependencies": {
"@types/emscripten": ">=1.39.6"
}
}
}
}
+3 -2
View File
@@ -9,13 +9,14 @@
"lint": "next lint"
},
"dependencies": {
"@yudiel/react-qr-scanner": "^2.2.1",
"axios": "^1.8.4",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"html5-qrcode": "^2.3.8",
"next": "15.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-hot-toast": "^2.5.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

@@ -7,18 +7,20 @@ import Select from '@/components/select';
import Input from '@/components/input';
import Button from '@/components/button';
import FloatingInput from '@/components/floating-input';
import { Pregunta, Seccion } from '@/types/cuestionario';
import { plantillasDisponibles } from '@/data/plantillas';
import { FormularioCreacion, PreguntaFormulario, SeccionFormulario } from '@/types/crear-formulario';
import toast from 'react-hot-toast';
export default function AdminFormPage() {
const [nombreFormulario, setNombreFormulario] = useState('');
const [descripcion, setDescripcion] = useState('');
const [secciones, setSecciones] = useState<Seccion[]>([]);
const [secciones, setSecciones] = useState<SeccionFormulario[]>([]);
const [paginaActual, setPaginaActual] = useState(1);
const [bannerPreview, setBannerPreview] = useState<string | null>(null);
const [tipoCuestionario, setTipoCuestionario] = useState<number | null>(null);
const [fechaInicio, setFechaInicio] = useState<string | null>(null);
const [fechaFin, setFechaFin] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const swapItems = <T,>(arr: T[], index1: number, index2: number): T[] => {
const result = [...arr];
@@ -58,10 +60,10 @@ export default function AdminFormPage() {
);
};
const actualizarSeccion = <K extends keyof Seccion>(
const actualizarSeccion = <K extends keyof SeccionFormulario>(
index: number,
campo: K,
valor: Seccion[K]
valor: SeccionFormulario[K]
) => {
setSecciones((prev) => {
const nuevas = [...prev];
@@ -90,8 +92,8 @@ export default function AdminFormPage() {
const actualizarPregunta = (
seccionIndex: number,
preguntaIndex: number,
campo: keyof Pregunta,
valor: Pregunta[keyof Pregunta]
campo: keyof PreguntaFormulario,
valor: PreguntaFormulario[keyof PreguntaFormulario]
) => {
setSecciones((prev) => {
const nuevas = [...prev];
@@ -103,12 +105,11 @@ export default function AdminFormPage() {
return nuevas;
});
};
const agregarOpcion = (seccionIndex: number, preguntaIndex: number) => {
const nuevas = [...secciones];
if (nuevas[seccionIndex]?.preguntas[preguntaIndex]?.opciones) {
nuevas[seccionIndex].preguntas[preguntaIndex].opciones.push({
valor: '',
});
nuevas[seccionIndex].preguntas[preguntaIndex].opciones!.push({ valor: '' });
}
setSecciones(nuevas);
};
@@ -135,18 +136,16 @@ export default function AdminFormPage() {
const nuevas = [...secciones];
if (
nuevas[seccionIndex]?.preguntas[preguntaIndex]?.opciones &&
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[opcionIndex]
nuevas[seccionIndex].preguntas[preguntaIndex].opciones![opcionIndex]
) {
nuevas[seccionIndex].preguntas[preguntaIndex].opciones[
opcionIndex
].valor = valor;
nuevas[seccionIndex].preguntas[preguntaIndex].opciones![opcionIndex].valor = valor;
}
setSecciones(nuevas);
};
const guardarFormulario = async () => {
if (fechaInicio === null || fechaFin === null) {
alert('Las fechas son obligatorias');
if (!fechaInicio || !fechaFin || !tipoCuestionario) {
alert('Todos los campos obligatorios deben estar llenos');
return;
}
@@ -155,27 +154,35 @@ export default function AdminFormPage() {
return `${fecha}${sufijo}`;
};
const formData = {
const formData: FormularioCreacion = {
nombre_form: nombreFormulario,
id_tipo_cuestionario: tipoCuestionario,
descripcion,
fecha_inicio: formatearFecha(fechaInicio, 'inicio'),
fecha_fin: formatearFecha(fechaFin, 'fin'),
descripcion,
id_tipo_cuestionario: tipoCuestionario,
evento: nombreFormulario, // temporalmente igual al nombre del form
secciones,
};
console.log('Formulario a guardar:', formData);
try {
const res = await axiosInstance.post('/cuestionario', formData);
setLoading(true);
await toast.promise(
axiosInstance.post('/cuestionario', formData),
{
loading: 'Guardando formulario...',
success: 'Formulario guardado exitosamente!',
error: 'Error al guardar el formulario.',
}
).then((res) => {
console.log('Formulario guardado:', res.data);
} catch (error) {
}).catch((error) => {
console.error('Error al guardar el formulario:', error);
}
}).finally(() => {
setLoading(false);
});
};
const seccionActual = secciones[paginaActual - 1];
const handleBannerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] || null;
if (file) {
@@ -187,67 +194,76 @@ export default function AdminFormPage() {
}
};
const seccionActual = secciones[paginaActual - 1];
return (
<>
<h2 className='my-4'>Crear nuevo formulario</h2>
<h2 className="my-4">Crear nuevo formulario</h2>
<Select
label='Usar una plantilla'
options={[
{ label: 'Selecciona una plantilla', value: '' },
...plantillasDisponibles.map((p) => ({
label: p.nombre,
value: p.id,
})),
]}
onChange={(e) => {
const plantilla = plantillasDisponibles.find(
(p) => p.id === e.target.value
);
if (plantilla) {
const {
nombre_form,
descripcion,
fecha_inicio,
fecha_fin,
id_tipo_cuestionario,
secciones,
} = plantilla.datos;
setNombreFormulario(nombre_form);
setDescripcion(descripcion);
setFechaInicio(fecha_inicio.slice(0, 10)); // YYYY-MM-DD
setFechaFin(fecha_fin.slice(0, 10));
setTipoCuestionario(id_tipo_cuestionario);
setSecciones(
secciones.map((seccion) => ({
...seccion,
preguntas: seccion.preguntas.map((pregunta) => ({
...pregunta,
tipo: pregunta.tipo as 'Abierta' | 'Cerrada' | 'Multiple',
})),
}))
);
setPaginaActual(1);
}
}}
/>
<div className="row">
{secciones.length === 0 &&
plantillasDisponibles.map((plantilla, index) => (
<div className="col-md-4 mb-3" key={plantilla.id}>
<h3>Plantillas</h3>
<div
className="box w-100 cursor-pointer"
onClick={() => {
const {
nombre_form,
descripcion,
fecha_inicio,
fecha_fin,
id_tipo_cuestionario,
secciones,
} = plantilla.datos;
setNombreFormulario(nombre_form);
setDescripcion(descripcion);
setFechaInicio(fecha_inicio.slice(0, 10));
setFechaFin(fecha_fin.slice(0, 10));
setTipoCuestionario(id_tipo_cuestionario);
setSecciones(
secciones.map((seccion) => ({
...seccion,
preguntas: seccion.preguntas.map((pregunta) => ({
...pregunta,
tipo: pregunta.tipo as
| 'Abierta'
| 'Cerrada'
| 'Multiple',
})),
}))
);
setPaginaActual(1);
}}
>
<Image
src={plantilla.imagen}
width={1000}
height={300}
alt={`Plantilla de formulario ${index + 1}`}
className="img-fluid rounded shadow-sm"
/>
</div>
</div>
))}
</div>
<div className='mb-4'>
<label className='form-label'>Banner del formulario</label>
<div className="mb-4">
<label className="form-label">Banner del formulario</label>
<input
type='file'
accept='image/*'
className='form-control mb-2'
type="file"
accept="image/*"
className="form-control mb-2"
onChange={handleBannerChange}
/>
{bannerPreview && (
<div className='text-center'>
<div className="text-center">
<Image
src={bannerPreview}
width={1000}
height={300}
alt='Ejemplo de banner'
className='rounded-4 shadow-sm'
alt="Ejemplo de banner"
className="rounded-4 shadow-sm"
style={{
objectFit: 'cover',
objectPosition: 'center',
@@ -258,7 +274,7 @@ export default function AdminFormPage() {
</div>
<Select
label='Tipo de cuestionario'
label="Tipo de cuestionario"
options={[
{ label: 'Seleccione un tipo', value: '' },
{ label: 'Tipo 1', value: 1 },
@@ -270,53 +286,53 @@ export default function AdminFormPage() {
/>
<Input
label='Nombre del formulario'
label="Nombre del formulario"
value={nombreFormulario}
onChange={(e) => setNombreFormulario(e.target.value)}
placeholder='Ingrese el nombre del formulario'
placeholder="Ingrese el nombre del formulario"
/>
<div className='mb-3'>
<label className='form-label'>Descripción</label>
<div className="mb-3">
<label className="form-label">Descripción</label>
<textarea
className='form-control bg-white'
className="form-control bg-white"
rows={3}
value={descripcion}
onChange={(e) => setDescripcion(e.target.value)}
/>
</div>
<div className='row'>
<div className='col-md-6'>
<div className="row">
<div className="col-md-6">
<Input
label='Fecha de inicio'
type='date'
label="Fecha de inicio"
type="date"
value={fechaInicio || ''}
onChange={(e) => setFechaInicio(e.target.value)}
/>
</div>
<div className='col-md-6'>
<div className="col-md-6">
<Input
label='Fecha de fin'
type='date'
label="Fecha de fin"
type="date"
value={fechaFin || ''}
onChange={(e) => setFechaFin(e.target.value)}
/>
</div>
</div>
<div className='mb-4'>
<Button outline onClick={agregarSeccion} icon='plus'>
<div className="mb-4">
<Button outline onClick={agregarSeccion} icon="plus">
Agregar Sección
</Button>
{seccionActual && (
<div className='border rounded p-3 mb-4'>
<div className='d-flex justify-content-between align-items-center'>
<div className="border rounded p-3 mb-4">
<div className="d-flex justify-content-between align-items-center">
<h5>Sección {paginaActual}</h5>
<div className='d-flex gap-2'>
<div className="d-flex gap-2">
<button
className='btn btn-sm btn-outline-secondary'
className="btn btn-sm btn-outline-secondary"
onClick={() =>
moverSeccion(paginaActual - 1, paginaActual - 2)
}
@@ -325,14 +341,14 @@ export default function AdminFormPage() {
</button>
<button
className='btn btn-sm btn-outline-secondary'
className="btn btn-sm btn-outline-secondary"
onClick={() => moverSeccion(paginaActual - 1, paginaActual)}
disabled={paginaActual === secciones.length}
>
</button>
<button
className='btn btn-sm btn-danger'
className="btn btn-sm btn-danger"
onClick={() => eliminarSeccion(paginaActual - 1)}
>
Eliminar sección
@@ -341,17 +357,17 @@ export default function AdminFormPage() {
</div>
<FloatingInput
label='Título de la sección'
label="Título de la sección"
className={{ container: 'mb-2' }}
value={seccionActual.titulo}
onChange={(e) =>
actualizarSeccion(paginaActual - 1, 'titulo', e.target.value)
}
placeholder='Título de la sección'
placeholder="Título de la sección"
/>
<textarea
className='form-control mb-3'
placeholder='Descripción de la sección'
className="form-control mb-3"
placeholder="Descripción de la sección"
value={seccionActual.descripcion}
onChange={(e) =>
actualizarSeccion(
@@ -363,12 +379,12 @@ export default function AdminFormPage() {
/>
{seccionActual.preguntas.map((pregunta, pIdx) => (
<div key={pIdx} className='border rounded p-2 mb-3'>
<div className='d-flex justify-content-between align-items-center'>
<div key={pIdx} className="border rounded p-2 mb-3">
<div className="d-flex justify-content-between align-items-center">
<strong>Pregunta {pIdx + 1}</strong>
<div className='d-flex gap-2'>
<div className="d-flex gap-2">
<button
className='btn btn-sm btn-outline-secondary'
className="btn btn-sm btn-outline-secondary"
onClick={() =>
moverPregunta(paginaActual - 1, pIdx, pIdx - 1)
}
@@ -377,7 +393,7 @@ export default function AdminFormPage() {
</button>
<button
className='btn btn-sm btn-outline-secondary'
className="btn btn-sm btn-outline-secondary"
onClick={() =>
moverPregunta(paginaActual - 1, pIdx, pIdx + 1)
}
@@ -386,7 +402,7 @@ export default function AdminFormPage() {
</button>
<button
className='btn btn-sm btn-outline-danger'
className="btn btn-sm btn-outline-danger"
onClick={() => eliminarPregunta(paginaActual - 1, pIdx)}
>
Eliminar
@@ -394,7 +410,7 @@ export default function AdminFormPage() {
</div>
</div>
<Input
label='Texto de la pregunta'
label="Texto de la pregunta"
className={{ container: 'my-2' }}
value={pregunta.titulo}
onChange={(e) =>
@@ -405,11 +421,11 @@ export default function AdminFormPage() {
e.target.value
)
}
placeholder='Texto de la pregunta'
placeholder="Texto de la pregunta"
/>
<Select
label='Tipo de pregunta'
label="Tipo de pregunta"
options={[
{ label: 'Abierta', value: 'Abierta' },
{ label: 'Cerrada', value: 'Cerrada' },
@@ -421,15 +437,15 @@ export default function AdminFormPage() {
paginaActual - 1,
pIdx,
'tipo',
e.target.value as Pregunta['tipo']
e.target.value as PreguntaFormulario['tipo']
)
}
/>
<div className='form-check form-switch mb-3'>
<div className="form-check form-switch mb-3">
<input
className='form-check-input'
type='checkbox'
className="form-check-input"
type="checkbox"
id={`obligatoria-${pIdx}`}
checked={pregunta.obligatoria}
onChange={(e) =>
@@ -442,7 +458,7 @@ export default function AdminFormPage() {
}
/>
<label
className='form-check-label'
className="form-check-label"
htmlFor={`obligatoria-${pIdx}`}
>
¿Es obligatoria?
@@ -453,16 +469,16 @@ export default function AdminFormPage() {
pregunta.tipo === 'Multiple') && (
<>
<button
className='btn btn-sm btn-outline-secondary mb-2'
className="btn btn-sm btn-outline-secondary mb-2"
onClick={() => agregarOpcion(paginaActual - 1, pIdx)}
>
+ Agregar opción
</button>
{(pregunta.opciones ?? []).map((op, oIdx) => (
<div key={oIdx} className='input-group mb-2'>
<div key={oIdx} className="input-group mb-2">
<input
type='text'
className='form-control bg-white'
type="text"
className="form-control bg-white"
placeholder={`Opción ${oIdx + 1}`}
value={op.valor}
onChange={(e) =>
@@ -475,7 +491,7 @@ export default function AdminFormPage() {
}
/>
<button
className='btn btn-outline-danger'
className="btn btn-outline-danger"
onClick={() =>
eliminarOpcion(paginaActual - 1, pIdx, oIdx)
}
@@ -490,7 +506,7 @@ export default function AdminFormPage() {
))}
<button
className='btn btn-sm btn-outline-primary'
className="btn btn-sm btn-outline-primary"
onClick={() => agregarPregunta(paginaActual - 1)}
>
+ Agregar pregunta
@@ -499,7 +515,7 @@ export default function AdminFormPage() {
)}
{secciones.length > 1 && (
<div className='d-flex justify-content-center mb-4'>
<div className="d-flex justify-content-center mb-4">
<Pagination
currentPage={paginaActual}
totalPages={secciones.length}
@@ -509,7 +525,7 @@ export default function AdminFormPage() {
)}
</div>
<Button onClick={guardarFormulario} color='success'>
<Button onClick={guardarFormulario} color="success">
Guardar formulario
</Button>
</>
+48 -87
View File
@@ -1,99 +1,60 @@
'use client';
import FormularioCard from '@/components/formulario-card';
import { GetCuestionario } from '@/types/cuestionario';
import React from 'react';
import { CuestionarioMetadata } from '@/types/cuestionario';
import axiosInstance from '@/utils/api-config';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
async function getEventos(): Promise<GetCuestionario[]> {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cuestionario`, {
method: 'GET',
cache: 'no-store', // para que no lo cachee ni en build ni entre requests
headers: {
'Content-Type': 'application/json',
},
});
export default function Page() {
const [cuestionarios, setCuestionarios] = useState<CuestionarioMetadata[]>(
[]
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
if (!res.ok) {
throw new Error(`Error ${res.status}: ${res.statusText}`);
}
useEffect(() => {
const fetchData = async () => {
try {
const res = await axiosInstance.get<CuestionarioMetadata[]>(
'/cuestionario'
);
if (res.status === 200) {
setCuestionarios(res.data);
} else {
setError('Error fetching data');
}
} catch (error) {
setError('Error de conexión');
console.error('Error fetching data:', error);
} finally {
setLoading(false);
}
};
const data: GetCuestionario[] = await res.json();
fetchData();
}, []);
return data;
} catch (error) {
console.error('Error en getEventos:', error);
throw error;
}
}
export default async function Page() {
let eventos;
try {
eventos = await getEventos();
} catch (error) {
console.error('Error al cargar los eventos:', error);
return <div>Error al cargar los eventos</div>;
}
return (
<>
{loading && <p>Cargando formularios...</p>}
{error && <p className='text-danger'>{error}</p>}
<article className='col-md-12'>
<header>
<h2>Formularios disponibles</h2>
</header>
<div className='cards-1 mt-5'>
<div className='container'>
<div className='row'>
{cuestionarios.map((cuestionario) => (
<div
className='col-md-4 my-4'
key={cuestionario.id_cuestionario}
>
<div className='card card-blog h-100'>
<div className='card-image'>
<Link
href={`/administrador/formulario/${cuestionario.id_cuestionario}`}
>
<img
className='img'
src='/feria.png'
alt='Banner formulario'
/>
<div className='card-caption'>
{cuestionario.nombre_form}
</div>
</Link>
<div className='ripple-cont'></div>
</div>
<div className='table px-3 pt-3 mb-0'>
<h6 className='category text-info'>
{cuestionario.id_cuestionario} -{' '}
{cuestionario.editable ? 'Editable' : 'No editable'}
</h6>
<p className='card-description'>
{cuestionario.descripcion.slice(0, 150)}...
</p>
<small className='text-muted'>
{cuestionario.contador_secciones} sección
{cuestionario.contador_secciones !== 1 && 'es'}
</small>
</div>
</div>
</div>
))}
{cuestionarios.length === 0 && !loading && (
<p className='text-center text-muted'>
No hay formularios disponibles.
</p>
)}
</div>
<div className="cards-1 mt-3">
<div className="container">
<div className="row">
{eventos.map((cuestionario) => (
<FormularioCard
key={cuestionario.id_cuestionario}
cuestionario={cuestionario}
link={`/administrador/formulario/${cuestionario.id_cuestionario}`}
/>
))}
{eventos.length === 0 && (
<p className="text-center text-muted">
No hay formularios disponibles.
</p>
)}
</div>
</div>
</article>
</div>
</>
);
}
+118 -55
View File
@@ -1,76 +1,139 @@
'use client';
import Header from '@/components/layout/header';
import React from 'react';
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 Header from '@/components/layout/header';
import Footer from '@/components/layout/footer';
const Html5QrcodePlugin = dynamic(() => import('@/components/qr-scanner'), {
ssr: false,
});
const Modal = dynamic(() => import('@/components/modal'), {
ssr: false,
});
const Modal = dynamic(() => import('@/components/modal'), { ssr: false });
export default function Page() {
const [text, setText] = React.useState<string>('');
const [showModal, setShowModal] = React.useState<boolean>(false);
const [scannedData, setScannedData] = useState<{
id_participante: number;
id_evento: number;
} | null>(null);
const onScanSuccess = (decodedText: string) => {
setText(decodedText);
setShowModal(true); // Muestra el modal al escanear
const [showModal, setShowModal] = useState(false);
const [enableScan, setEnableScan] = useState(true);
const [statusMessage, setStatusMessage] = useState('');
const handleScan = (rawValue: string) => {
if (!enableScan) return;
try {
const data = JSON.parse(rawValue);
if (data.id_participante && data.id_evento) {
setScannedData({
id_participante: data.id_participante,
id_evento: data.id_evento,
});
setEnableScan(false);
setShowModal(true);
} 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 onScanError = (errorMessage: string) => {
console.warn('QR Error:', errorMessage);
};
const handleConfirm = async () => {
if (!scannedData) return;
try {
const response = await axiosInstance.post(
`/participante-evento/asistencia/${scannedData.id_participante}/${scannedData.id_evento}`
);
console.log('Asistencia registrada:', response.data);
} catch (err) {
console.error('Error en la petición:', err);
setStatusMessage('❌ Error de red al registrar asistencia');
}
const handleCloseModal = () => {
setShowModal(false);
setEnableScan(true);
setScannedData(null);
};
return (
<div className='d-flex flex-column min-vh-100'>
<Header small />
<main className='py-3'>
<div className='container text-center'>
<h1 className='mb-4'>Lectura de QRs</h1>
<div className="d-flex flex-column min-vh-100">
<Header />
<main className='container flex-grow-1 d-flex flex-column align-items-center justify-content-center'>
<h1 className="mb-4">Lectura de QRs</h1>
<div className='mb-4'>
<Html5QrcodePlugin
fps={10}
qrbox={500}
qrCodeSuccessCallback={onScanSuccess}
qrCodeErrorCallback={onScanError}
/>
</div>
{enableScan ? (
<>
<div style={{ width: '100%', maxWidth: '400px' }}>
<Scanner
onScan={(codes) => {
if (codes.length > 0) {
handleScan(codes[0].rawValue);
}
}}
onError={(err) => console.error('QR Error:', err)}
constraints={{ facingMode: 'environment' }}
/>
</div>
<Button
onClick={() => setEnableScan(false)}
variant="outline-danger"
className="mt-3"
>
Detener cámara
</Button>
</>
) : (
<>
<p className="text-muted mb-3">La cámara está detenida.</p>
<Button
onClick={() => setEnableScan(true)}
variant="outline-primary"
>
Activar cámara
</Button>
</>
)}
<p className='mt-4'>
{text
? `Texto escaneado: ${text}`
: 'Escanea un código QR para ver el texto aquí.'}
</p>
</div>
{statusMessage && <p className="mt-3">{statusMessage}</p>}
<Modal
isVisible={showModal}
size="md"
onClose={() => {
setShowModal(false);
setEnableScan(true);
setScannedData(null);
}}
closeButton
className={{
content: 'p-3',
body: 'text-center',
}}
>
<h5 className="mb-3">Datos escaneados</h5>
{scannedData ? (
<>
<p>
<strong>ID Participante:</strong> {scannedData.id_participante}
</p>
<p>
<strong>ID Evento:</strong> {scannedData.id_evento}
</p>
<Button variant="success" onClick={handleConfirm}>
Confirmar lectura
</Button>
</>
) : (
<p>QR inválido</p>
)}
</Modal>
</main>
<Modal
isVisible={showModal}
size='md'
onClose={handleCloseModal}
closeButton
className={{
content: 'p-3',
body: 'text-center',
}}
>
<h5 className='mb-3'>Texto escaneado</h5>
<p>{text}</p>
<Button variant='success' onClick={handleCloseModal}>
Confirmar lectura
</Button>
</Modal>
<Footer />
</div>
);
}
+2 -2
View File
@@ -4,9 +4,9 @@ import React from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className='d-flex flex-column min-vh-100'>
<div className="d-flex flex-column min-vh-100">
<Header />
<main className='container flex-grow-1'>{children}</main>
<main className="container flex-grow-1">{children}</main>
<Footer />
</div>
);
+64
View File
@@ -0,0 +1,64 @@
import ClientCarousel from '@/client-components/client-carousel';
import FormularioCard from '@/components/formulario-card';
import { GetCuestionario } from '@/types/cuestionario';
import React from 'react';
async function getEventos(): Promise<GetCuestionario[]> {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cuestionario`, {
method: 'GET',
cache: 'no-store', // para que no lo cachee ni en build ni entre requests
headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) {
throw new Error(`Error ${res.status}: ${res.statusText}`);
}
const data: GetCuestionario[] = await res.json();
return data;
} catch (error) {
console.error('Error en getEventos:', error);
throw error;
}
}
export default async function Page() {
let eventos;
try {
eventos = await getEventos();
} catch (error) {
console.error('Error al cargar los eventos:', error);
return <div>Error al cargar los eventos</div>;
}
return (
<>
<div className="mx-auto my-5">
<ClientCarousel />
</div>
<div className="cards-1 mt-5">
<div className="container">
<div className="row">
{eventos.map((cuestionario) => (
<FormularioCard
key={cuestionario.id_cuestionario}
cuestionario={cuestionario}
link={`/registro/${cuestionario.id_cuestionario}`}
/>
))}
{eventos.length === 0 && (
<p className="text-center text-muted">
No hay formularios disponibles.
</p>
)}
</div>
</div>
</div>
</>
);
}
@@ -1,28 +1,38 @@
'use client';
import Button from '@/components/button';
import CheckboxOptionGroup from '@/components/checkbox-option-group';
import Input from '@/components/input';
import RadioOptionGroup from '@/components/radio-option-group';
import { Cuestionario } from '@/types/cuestionario';
import { CuestionarioResponse } from '@/types/responder-formulario';
import axiosInstance from '@/utils/api-config';
import { useParams } from 'next/navigation';
import Image from 'next/image';
import { useParams, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
export default function Page() {
const router = useRouter();
const params = useParams<{ id_formulario: string }>();
const id_formulario = params?.id_formulario;
const [cuestionario, setCuestionario] = useState<Cuestionario | null>(null);
const [cuestionario, setCuestionario] = useState<CuestionarioResponse | null>(
null
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [respuestas, setRespuestas] = useState<Record<string, any>>({});
const [respuestas, setRespuestas] = useState<
Record<number, string | number | Array<string | number>>
>({});
const [isSubmitting, setIsSubmitting] = useState(false); // New state for submission
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const { data } = await axiosInstance.get<Cuestionario>(
const { data } = await axiosInstance.get<CuestionarioResponse>(
`/cuestionario/${id_formulario}/formulario`
);
if (data) {
console.log(data);
setCuestionario(data);
} else {
setError('Error fetching data');
@@ -42,22 +52,26 @@ export default function Page() {
setRespuestas((prev) => ({ ...prev, [id]: value }));
};
const handleRadioChange = (id: number, opcion: any) => {
const handleRadioChange = (id: number, opcion: { value: number }) => {
setRespuestas((prev) => ({ ...prev, [id]: opcion.value }));
};
const handleCheckboxChange = (id: number, opcion: any) => {
const handleCheckboxChange = (id: number, opcion: { value: number }) => {
setRespuestas((prev) => {
const actuales = prev[id] || [];
const actuales: number[] = Array.isArray(prev[id])
? (prev[id] as number[])
: [];
const existe = actuales.includes(opcion.value);
const nuevos = existe
? actuales.filter((v: any) => v !== opcion.value)
? actuales.filter((v: number) => v !== opcion.value)
: [...actuales, opcion.value];
return { ...prev, [id]: nuevos };
});
};
const formatearRespuestas = async () => {
if (!validarRespuestasObligatorias()) return;
const respuestasArray = Object.entries(respuestas).flatMap(
([id, valor]) => {
if (Array.isArray(valor)) {
@@ -83,32 +97,87 @@ export default function Page() {
fecha_envio: new Date().toISOString(),
};
console.log(resultado);
setIsSubmitting(true); // Set submitting state to true
toast
.promise(
axiosInstance.post('/cuestionario-respondido/submit', resultado),
{
loading: 'Enviando formulario...',
success: 'Formulario enviado con éxito!',
error: 'Error al enviar el formulario.',
}
)
.then(() => {
router.push('/');
})
.catch((error) => {
console.error('Error al enviar el formulario:', error);
})
.finally(() => {
setIsSubmitting(false); // Reset submitting state
});
};
try {
const res = await axiosInstance.post(
'/cuestionario-respondido/submit',
resultado
);
const validarRespuestasObligatorias = (): boolean => {
const faltantes: number[] = [];
console.log(res);
} catch (error) {
console.error('Error al enviar el formulario:', error);
cuestionario?.cuestionario.secciones.forEach((seccion) => {
seccion.preguntas.forEach(({ pregunta }) => {
if (pregunta.obligatoria) {
const valor = respuestas[pregunta.id_pregunta];
const tipo = pregunta.tipo_pregunta.tipo_pregunta;
const estaRespondida =
(tipo === 'Abierta' &&
typeof valor === 'string' &&
valor.trim() !== '') ||
(tipo === 'Cerrada' && valor !== undefined) ||
(tipo === 'Multiple' && Array.isArray(valor) && valor.length > 0);
if (!estaRespondida) {
faltantes.push(pregunta.id_pregunta);
}
}
});
});
if (faltantes.length > 0) {
alert(`Responde todas las preguntas obligatorias antes de enviar.`);
return false;
}
return true;
};
if (loading) return <p>Cargando...</p>;
if (error) return <p className='text-danger'>{error}</p>;
if (error) return <p className="text-danger">{error}</p>;
if (!cuestionario) return <p>No hay datos</p>;
return (
<div className='container py-4'>
<div>
<div className="container py-4">
<Button icon="arrow-left" onClick={() => router.back()}>
Volver
</Button>
<div className="text-center mb-4">
<Image
src={'/banner1.png'}
width={1000}
height={300}
alt="Ejemplo de banner"
className="rounded-4 shadow-sm img-fluid"
style={{
objectFit: 'cover',
objectPosition: 'center',
height: 300,
}}
/>
</div>
<div className="px-lg-5 mx-lg-5">
<h2>{cuestionario.cuestionario.nombre_form}</h2>
<p>{cuestionario.cuestionario.descripcion}</p>
{cuestionario.cuestionario.secciones.map((seccion, index) => (
<section key={index} className='mb-4'>
<section key={index} className="mb-4">
<h4>{seccion.seccion.titulo}</h4>
<p>{seccion.seccion.descripcion}</p>
@@ -123,7 +192,11 @@ export default function Page() {
key={id}
label={pregunta.pregunta}
name={`pregunta_${id}`}
value={respuestas[id] || ''}
value={
Array.isArray(respuestas[id])
? respuestas[id].join(', ')
: respuestas[id] || ''
}
onChange={(e) => handleInputChange(id, e.target.value)}
required={pregunta.obligatoria}
/>
@@ -131,33 +204,47 @@ export default function Page() {
}
if (tipo === 'Cerrada' || tipo === 'Multiple') {
const opciones = pregunta.opciones.map((o) => ({
const opciones = (pregunta.opciones ?? []).map((o) => ({
label: o.opcion.opcion,
value: o.opcion.id_opcion,
}));
if (tipo === 'Multiple') {
return (
<div key={id} className='mb-3'>
<div key={id} className="mb-3">
<p>{pregunta.pregunta}</p>
<CheckboxOptionGroup
name={`pregunta_${id}`}
options={opciones}
selectedValues={respuestas[id] || []}
onChange={(opcion) => handleCheckboxChange(id, opcion)}
selectedValues={
Array.isArray(respuestas[id]) ? respuestas[id] : []
}
onChange={(opcion) => {
if (typeof opcion.value === 'number') {
handleCheckboxChange(id, { value: opcion.value });
} else {
console.error('Invalid value type:', opcion.value);
}
}}
/>
</div>
);
}
return (
<div key={id} className='mb-3'>
<div key={id} className="mb-3">
<p>{pregunta.pregunta}</p>
<RadioOptionGroup
name={`pregunta_${id}`}
options={opciones}
selectedValue={respuestas[id]}
onChange={(opcion) => handleRadioChange(id, opcion)}
onChange={(opcion) => {
if (typeof opcion.value === 'number') {
handleRadioChange(id, { value: opcion.value });
} else {
console.error('Invalid value type:', opcion.value);
}
}}
disabled={false}
/>
</div>
@@ -170,11 +257,12 @@ export default function Page() {
))}
<button
type='button'
className='btn btn-primary'
type="button"
className="btn btn-primary"
onClick={formatearRespuestas}
disabled={isSubmitting} // Disable button while submitting
>
Enviar
{isSubmitting ? 'Enviando...' : 'Enviar'}
</button>
</div>
</div>
+2
View File
@@ -4,6 +4,7 @@ import { Roboto } from "next/font/google";
import "bootstrap-icons/font/bootstrap-icons.css";
import "@/styles/sass/bootstrap.scss";
import BootstrapClient from "@/components/bootstrap-client";
import { Toaster } from "react-hot-toast";
const roboto = Roboto({
weight: ["100", "300", "400", "500", "700", "900"],
@@ -27,6 +28,7 @@ export default function RootLayout({
<html lang="en">
<body className={roboto.className}>
<BootstrapClient />
<Toaster />
{children}
</body>
</html>
-150
View File
@@ -1,150 +0,0 @@
'use client';
import Footer from '@/components/layout/footer';
import Header from '@/components/layout/header';
import React from 'react';
/* import { res } from '@/data/cuestionario';
import RadioOptionGroup, { RadioOption } from '@/components/radio-option-group';
import CheckboxOptionGroup, {
CheckboxOption,
} from '@/components/checkbox-option-group';
import Pagination from '@/components/pagination';
import Image from 'next/image';
type Respuesta = string | number | number[];
*/
export default function Page() {
/* const [respuestas, setRespuestas] = useState<Record<number, Respuesta>>({});
const [paginaActual, setPaginaActual] = useState(1);
const totalPaginas = res.cuestionario.secciones.length;
const handleRespuesta = (idPregunta: number, valor: string | number) => {
setRespuestas((prev) => ({ ...prev, [idPregunta]: valor }));
};
const handleMultipleRespuesta = (
idPregunta: number,
opcion: CheckboxOption<number>
) => {
setRespuestas((prev) => {
const current = Array.isArray(prev[idPregunta])
? (prev[idPregunta] as number[])
: [];
const updated = current.includes(opcion.value)
? current.filter((val) => val !== opcion.value)
: [...current, opcion.value];
return { ...prev, [idPregunta]: updated };
});
};
const imprimirRespuestas = () => {
console.log('Respuestas del cuestionario:', respuestas);
alert('Respuestas impresas en consola');
};
const seccion = res.cuestionario.secciones[paginaActual - 1];
*/
return (
<div className='d-flex flex-column min-vh-100'>
<Header />
{/* <div className='container flex-grow-1'>
<div className='text-center my-4'>
<Image
src={'/banner.png'}
width={1000}
height={300}
alt='Ejemplo de banner'
className='rounded-4 shadow img-fluid'
style={{
objectFit: 'cover',
objectPosition: 'top',
height: 300,
}}
/>
</div>
<h1 className='mb-4'>{res.cuestionario.nombre_form}</h1>
<p className='mb-5 text-muted'>{res.cuestionario.descripcion}</p>
<div className='mb-5 mx-md-5 px-md-5'>
<h3>{seccion.seccion.titulo}</h3>
<p className='text-muted'>{seccion.seccion.descripcion}</p>
{seccion.preguntas.map((preguntaWrap) => {
const pregunta = preguntaWrap.pregunta;
const id = pregunta.id_pregunta;
if (pregunta.tipo_pregunta.tipo_pregunta === 'Cerrada') {
const opciones: RadioOption<number>[] = pregunta.opciones.map(
(op) => ({
label: op.opcion.opcion,
value: op.opcion.id_opcion,
})
);
return (
<div key={id} className='mt-4'>
<strong>{pregunta.pregunta}</strong>
<RadioOptionGroup
name={`pregunta-${id}`}
options={opciones}
selectedValue={respuestas[id] as number}
onChange={(op) => handleRespuesta(id, op.value)}
/>
</div>
);
} else if (pregunta.tipo_pregunta.tipo_pregunta === 'Abierta') {
return (
<div key={id} className='mt-4'>
<strong>{pregunta.pregunta}</strong>
<textarea
className='form-control bg-white mt-2'
rows={3}
value={
typeof respuestas[id] === 'string' ? respuestas[id] : ''
}
onChange={(e) => handleRespuesta(id, e.target.value)}
/>
</div>
);
} else if (pregunta.tipo_pregunta.tipo_pregunta === 'Multiple') {
const opciones: CheckboxOption<number>[] = pregunta.opciones.map(
(op) => ({
label: op.opcion.opcion,
value: op.opcion.id_opcion,
})
);
return (
<div key={id} className='mt-4'>
<strong>{pregunta.pregunta}</strong>
<CheckboxOptionGroup
name={`pregunta-${id}`}
options={opciones}
selectedValues={(respuestas[id] as number[]) || []}
onChange={(op) => handleMultipleRespuesta(id, op)}
/>
</div>
);
}
return null;
})}
</div>
<div className='d-flex justify-content-center mb-4'>
<Pagination
currentPage={paginaActual}
totalPages={totalPaginas}
onPageChange={setPaginaActual}
/>
</div>
<div className='text-center mb-5'>
<button className='btn btn-primary' onClick={imprimirRespuestas}>
Imprimir respuestas en consola
</button>
</div>
</div> */}
<Footer />
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
'use client';
import dynamic from 'next/dynamic';
import React from 'react';
const Carousel = dynamic(() => import('@/components/carousel'), {
ssr: false,
});
export default function ClientCarousel() {
return <Carousel />;
}
+82
View File
@@ -0,0 +1,82 @@
import Image from 'next/image';
import React from 'react';
interface CarouselProps {
id?: string;
images?: {
src: string;
alt?: string;
}[];
}
export default function Carousel({
id = 'carouselExample',
images = [
{
src: '/banner1.png',
alt: 'Imagen de ejemplo',
},
{
src: '/banner2.png',
alt: 'Imagen de ejemplo',
},
],
}: CarouselProps) {
if (!images || images.length === 0) {
return <div>No hay imágenes para mostrar</div>;
}
return (
<div id={id} className="carousel slide" data-bs-ride="carousel">
<div className="carousel-inner">
{images.map((img, index) => (
<div
key={index}
className={`carousel-item ${index === 0 ? 'active' : ''}`}
>
<Image
src={img.src}
width={1000}
height={400}
alt="Ejemplo de banner"
className="d-block w-100 rounded-3"
style={{
objectFit: 'cover',
objectPosition: 'center',
}}
/>
</div>
))}
</div>
{images.length > 1 && (
<>
<button
className="carousel-control-prev"
type="button"
data-bs-target={`#${id}`}
data-bs-slide="prev"
>
<span
className="carousel-control-prev-icon"
aria-hidden="true"
></span>
<span className="visually-hidden">Anterior</span>
</button>
<button
className="carousel-control-next"
type="button"
data-bs-target={`#${id}`}
data-bs-slide="next"
>
<span
className="carousel-control-next-icon"
aria-hidden="true"
></span>
<span className="visually-hidden">Siguiente</span>
</button>
</>
)}
</div>
);
}
+56
View File
@@ -0,0 +1,56 @@
'use client';
import Link from 'next/link';
import { GetCuestionario } from '@/types/cuestionario';
import React, { useState } from 'react';
import Image from 'next/image';
interface Props {
cuestionario: GetCuestionario;
link: string;
}
export default function FormularioCard({ cuestionario, link }: Props) {
const [verMas, setVerMas] = useState(false);
const descripcion = cuestionario.descripcion;
const limite = 100;
const descripcionRecortada =
descripcion.length > limite && !verMas
? `${descripcion.slice(0, limite)}...`
: descripcion;
return (
<div className="col-md-4 my-4">
<div className="card card-blog d-flex flex-column justify-content-between">
<div>
<div className="card-image">
<Link href={link}>
<Image width={400} height={300} className="img" src="/banner1.png" alt="Banner formulario" />
<div className="card-caption">{cuestionario.nombre_form}</div>
</Link>
<div className="ripple-cont"></div>
</div>
<div className="table px-3 pt-3 mb-0">
<h6 className="category text-info">
{cuestionario.id_tipo_cuestionario === 1 ? 'Feria' : 'Evento'}
</h6>
<p className="card-description mb-0">
{descripcionRecortada}
{descripcion.length > limite && (
<button
onClick={() => setVerMas(!verMas)}
className="btn btn-link btn-sm p-0 ms-1 align-baseline"
style={{ fontSize: '0.875rem' }}
>
{verMas ? 'Ver menos' : 'Ver más'}
</button>
)}
</p>
</div>
</div>
</div>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
// components/QrReader.tsx
'use client';
import { Scanner } from '@yudiel/react-qr-scanner';
export default function QrReader() {
return <Scanner onScan={(result) => console.log(result)} />;
}
-70
View File
@@ -1,70 +0,0 @@
// file: Html5QrcodePlugin.tsx
'use client';
import { Html5QrcodeScanner, Html5QrcodeCameraScanConfig } from 'html5-qrcode';
import { useEffect } from 'react';
const qrcodeRegionId = 'html5qr-code-full-region';
interface Html5QrcodePluginProps extends Partial<Html5QrcodeCameraScanConfig> {
verbose?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
qrCodeSuccessCallback: (decodedText: string, decodedResult: any) => void;
qrCodeErrorCallback?: (errorMessage: string) => void;
}
const createConfig = (
props: Html5QrcodePluginProps
): Html5QrcodeCameraScanConfig => {
const config: Html5QrcodeCameraScanConfig = { fps: 10 }; // Default fps value set to 10
if (props.fps !== undefined) {
config.fps = props.fps;
}
if (props.qrbox !== undefined) {
config.qrbox = props.qrbox;
}
if (props.aspectRatio !== undefined) {
config.aspectRatio = props.aspectRatio;
}
if (props.disableFlip !== undefined) {
config.disableFlip = props.disableFlip;
}
return config;
};
const Html5QrcodePlugin: React.FC<Html5QrcodePluginProps> = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
// Previene ejecución en SSR
if (typeof window === 'undefined') return;
const config = createConfig(props);
const verbose = props.verbose === true;
if (!props.qrCodeSuccessCallback) {
throw new Error('qrCodeSuccessCallback is required.');
}
const html5QrcodeScanner = new Html5QrcodeScanner(
qrcodeRegionId,
config,
verbose
);
html5QrcodeScanner.render(
props.qrCodeSuccessCallback,
props.qrCodeErrorCallback
);
return () => {
html5QrcodeScanner.clear().catch((error) => {
console.error('Failed to clear html5QrcodeScanner: ', error);
});
};
}, []);
return <div id={qrcodeRegionId} />;
};
export default Html5QrcodePlugin;
+1 -1
View File
@@ -1,4 +1,4 @@
export const res: CuestionarioCompleto = {
export const res = {
tipo_cuestionario: {
id_tipo_cuestionario: 1,
tipo_cuestionario: 'Encuesta',
+1 -2
View File
@@ -1,7 +1,6 @@
import { FormularioCreateDto } from '@/types/cuestionario';
export const plantillasDisponibles = [
{
imagen: '/banner1.png',
nombre: 'Registro para la Feria',
id: 'feria-sexualidad',
datos: {
+4
View File
@@ -117,3 +117,7 @@ ul.list-unstyled li {
}
}
}
.cursor-pointer {
cursor: pointer;
}
+26
View File
@@ -0,0 +1,26 @@
export interface FormularioCreacion {
nombre_form: string;
descripcion: string;
fecha_inicio: string; // ISO 8601 date string
fecha_fin: string;
id_tipo_cuestionario: number;
evento: string;
secciones: SeccionFormulario[];
}
export interface SeccionFormulario {
titulo: string;
descripcion: string;
preguntas: PreguntaFormulario[];
}
export interface PreguntaFormulario {
titulo: string;
tipo: 'Cerrada' | 'Abierta' | 'Multiple';
opciones?: OpcionFormulario[];
obligatoria: boolean;
}
export interface OpcionFormulario {
valor: string;
}
+18 -65
View File
@@ -1,3 +1,21 @@
/**
* Respuesta del endpoint GET /cuestionario
*/
export interface GetCuestionario {
id_cuestionario: number;
nombre_form: string;
descripcion: string;
contador_secciones: number;
editable: boolean;
fecha_fin: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
fecha_inicio: string; // Formato ISO: 'YYYY-MM-DDTHH:mm:ss'
id_tipo_cuestionario: number;
}
/* */
/* */
/* */
export interface FormularioCreateDto {
nombre_form: string;
descripcion: string;
@@ -14,13 +32,6 @@ export interface Seccion {
preguntas: Pregunta[];
}
export interface Pregunta {
titulo: string;
tipo: 'Abierta' | 'Cerrada' | 'Multiple';
opciones?: Opcion[]; // Solo se requiere en tipos Cerrada/Multiple
obligatoria: boolean;
}
export interface Opcion {
valor: string;
}
@@ -39,61 +50,3 @@ export interface CuestionarioMetadata {
}
/* ESto es para repsonder el formulario */
export interface cuestionarioExtendido extends CuestionarioMetadata {
secciones: SeccionCompleta[];
}
export interface Cuestionario {
cuestionario: cuestionarioExtendido;
evento: string | null;
tipo_cuestionario: {
id_tipo_cuestionario: number;
tipo_cuestionario: string;
};
}
export interface SeccionCompleta {
id_cuestiorio_seccion: number;
posicion: number;
seccion: {
id_seccion: number;
titulo: string;
descripcion: string;
contador_preguntas: number;
};
preguntas: SeccionPregunta[];
}
export interface SeccionPregunta {
id_seccion_pregunta: number;
posicion: number;
pregunta: PreguntaCompleta;
}
export interface PreguntaCompleta {
contador_opcion: number;
id_opcion_dependiente: number | null;
id_pregunta: number;
id_tipo_pregunta: number;
obligatoria: boolean;
opciones: OpcionCompleta[];
pregunta: string;
tipo_pregunta: {
id_tipo: number;
tipo_pregunta: string;
};
}
export interface OpcionCompleta {
id_opcion: number;
id_pregunta_opcion: number;
opcion: {
id_opcion: number;
opcion: string;
};
posicion: number;
}
export interface GetFormularioAResponder {
cues;
}
+73
View File
@@ -0,0 +1,73 @@
import { GetCuestionario } from "./cuestionario";
export interface CuestionarioResponse {
tipo_cuestionario: TipoCuestionario;
evento: Evento;
cuestionario: Cuestionario;
}
export interface TipoCuestionario {
id_tipo_cuestionario: number;
tipo_cuestionario: string;
}
export interface Evento {
id_evento: number;
nombre_evento: string;
tipo_evento: string;
fecha_inicio: string; // ISO format date string
fecha_fin: string;
}
export interface Cuestionario extends GetCuestionario {
secciones: CuestionarioSeccion[];
}
export interface CuestionarioSeccion {
id_cuestionario_seccion: number;
posicion: number;
seccion: Seccion;
preguntas: SeccionPregunta[];
}
export interface Seccion {
id_seccion: number;
contador_pregunta: number;
descripcion: string;
titulo: string;
}
export interface SeccionPregunta {
id_seccion_pregunta: number;
posicion: number;
pregunta: Pregunta;
}
export interface Pregunta {
id_pregunta: number;
pregunta: string;
contador_opcion: number;
obligatoria: boolean;
id_tipo_pregunta: number;
id_opcion_dependiente: number | null;
tipo_pregunta: TipoPregunta;
opciones: PreguntaOpcion[];
}
export interface TipoPregunta {
id_tipo: number;
tipo_pregunta: string;
}
export interface PreguntaOpcion {
id_pregunta_opcion: number;
posicion: number;
id_opcion: number;
opcion: Opcion;
}
export interface Opcion {
id_opcion: number;
opcion: string;
}