Lectura qr

This commit is contained in:
jalvarado
2025-04-02 11:27:21 -06:00
parent d2759c0afe
commit 236522a637
13 changed files with 356 additions and 59 deletions
+18 -1
View File
@@ -10,12 +10,14 @@
"dependencies": {
"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"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/bootstrap": "^5.2.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -1157,7 +1159,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -1203,6 +1204,16 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/bootstrap": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
"integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.2"
}
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -3446,6 +3457,12 @@
"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",
+2
View File
@@ -11,12 +11,14 @@
"dependencies": {
"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"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/bootstrap": "^5.2.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+15
View File
@@ -0,0 +1,15 @@
import Footer from '@/components/layout/footer';
import Header from '@/components/layout/header';
import React from 'react';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className='d-flex flex-column min-vh-100'>
<Header />
<main className='flex-grow-1 d-flex flex-column justify-content-center align-items-center'>
<div className='container'>{children}</div>
</main>
<Footer />
</div>
);
}
@@ -0,0 +1,5 @@
import React from 'react';
export default function Page() {
return <div>Page</div>;
}
+33
View File
@@ -0,0 +1,33 @@
'use client';
import Button from '@/components/button';
import Input from '@/components/input';
import { useRouter } from 'next/navigation';
import React from 'react';
export default function Page() {
const router = useRouter();
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Aquí puedes agregar la lógica para manejar el inicio de sesión
// Por ejemplo, validar las credenciales y redirigir al usuario
router.push('/staff');
};
return (
<div className='d-flex flex-column justify-content-center align-items-center'>
<h1 className='text-dorado'>Iniciar sesión</h1>
<h2 className='text-azul'>Staff</h2>
<form className='w-300px' onSubmit={handleOnSubmit}>
<Input label='Usuario' />
<Input type='password' label='Contraseña' />
<div className='mb-3'>
<Button className='w-100' type='submit'>
Iniciar sesión
</Button>
</div>
</form>
</div>
);
}
+20 -19
View File
@@ -206,25 +206,6 @@ export default function AdminFormPage() {
)}
</div>
<div className='mb-3'>
<label className='form-label'>Nombre del formulario</label>
<input
type='text'
className='form-control'
value={nombreFormulario}
onChange={(e) => setNombreFormulario(e.target.value)}
/>
</div>
<div className='mb-3'>
<label className='form-label'>Descripción</label>
<textarea
className='form-control'
rows={3}
value={descripcion}
onChange={(e) => setDescripcion(e.target.value)}
/>
</div>
<div className='mb-4'>
<label className='form-label d-block'>Audiencia del formulario</label>
<div className='form-check form-check-inline'>
@@ -271,6 +252,26 @@ export default function AdminFormPage() {
</div>
</div>
<div className='mb-3'>
<label className='form-label'>Nombre del formulario</label>
<input
type='text'
className='form-control'
value={nombreFormulario}
onChange={(e) => setNombreFormulario(e.target.value)}
/>
</div>
<div className='mb-3'>
<label className='form-label'>Descripción</label>
<textarea
className='form-control'
rows={3}
value={descripcion}
onChange={(e) => setDescripcion(e.target.value)}
/>
</div>
<div className='mb-4'>
<button
className='btn btn-outline-primary mb-3'
+51
View File
@@ -0,0 +1,51 @@
'use client';
import Header from '@/components/layout/header';
import QrScanner from '@/components/qr-scanner';
import dynamic from 'next/dynamic';
import React, { useState } from 'react';
const Modal = dynamic(() => import('@/components/modal'), {
ssr: false,
});
export default function Page() {
const [resultado, setResultado] = useState<string | null>(null);
const [mostrarModal, setMostrarModal] = useState(false);
const handleScan = (text: string) => {
setResultado(text);
setMostrarModal(true);
};
const handleConfirm = () => {
// Aquí haces algo con el QR (guardar, redirigir, etc)
alert('Confirmado: ' + resultado);
setMostrarModal(false);
setResultado(null); // Limpiar para volver a escanear
};
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>
{!resultado && <QrScanner onScan={handleScan} />}
<Modal
isVisible={mostrarModal}
onClose={() => setMostrarModal(false)}
size='md'
>
<h5 className='mb-3'>Resultado del QR</h5>
<pre className='bg-light p-2 rounded'>{resultado}</pre>
<button className='btn btn-primary mt-3' onClick={handleConfirm}>
Confirmar
</button>
</Modal>
</div>
</main>
</div>
);
}
+98
View File
@@ -0,0 +1,98 @@
'use client';
import React, { useEffect, useRef } from 'react';
import { Modal as BootstrapModal } from 'bootstrap';
interface ModalProps {
children?: React.ReactNode;
isVisible: boolean;
closeButton?: boolean;
size: 'sm' | 'md' | 'lg' | 'xl';
className?: {
dialog?: string;
content?: string;
body?: string;
};
onClose: () => void;
}
export default function Modal({
children,
isVisible,
size,
className,
closeButton = true,
onClose,
}: ModalProps) {
const ref = useRef<HTMLDivElement | null>(null);
const modalInstance = useRef<BootstrapModal | null>(null);
const modalSizeClass = size ? `modal-${size}` : '';
useEffect(() => {
const node = ref.current;
if (node) {
modalInstance.current = new BootstrapModal(node);
const handleClose = () => {
const elements = document.querySelectorAll<HTMLDivElement>(
'.modal-backdrop.fade.show'
);
if (elements.length > 0) {
elements.forEach((element) => {
element.remove();
});
document.body.removeAttribute('style');
}
onClose();
};
node.addEventListener('hidden.bs.modal', handleClose);
return () => {
if (node) {
node.removeEventListener('hidden.bs.modal', handleClose);
}
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isVisible) {
modalInstance.current?.show();
} else {
modalInstance.current?.hide();
}
}, [isVisible]);
return (
<div
ref={ref}
className="modal fade"
tabIndex={-1}
aria-labelledby="modal"
aria-hidden="true"
>
<div
className={`modal-dialog modal-dialog-centered ${className?.dialog ?? ''} ${modalSizeClass}`}
>
<div className={`modal-content ${className?.content}`}>
{closeButton && (
<div className="position-relative">
<button
type="button"
className="btn-close position-absolute top-0 end-0 m-2"
data-bs-dismiss="modal"
aria-label="Close"
style={{
zIndex: 999,
}}
></button>
</div>
)}
<div className={`modal-body ${className?.body}`}>{children}</div>
</div>
</div>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
// components/QrScanner.tsx
'use client';
import { useEffect } from 'react';
import { Html5Qrcode } from 'html5-qrcode';
interface Props {
onScan: (text: string) => void;
}
export default function QrScanner({ onScan }: Props) {
useEffect(() => {
const scanner = new Html5Qrcode('qr-reader');
scanner
.start(
{ facingMode: 'environment' },
{ fps: 10, qrbox: 250 },
(decodedText) => {
onScan(decodedText);
scanner.stop().then(() => scanner.clear());
},
(error) => {
// puedes ignorar errores de escaneo frecuentes
console.error('Error de escaneo:', error);
}
)
.catch((err) => console.error('Error al iniciar el lector QR:', err));
return () => {
scanner.stop().then(() => scanner.clear());
};
}, [onScan]);
return (
<div
id='qr-reader'
style={{ width: '100%', maxWidth: 400, margin: '0 auto' }}
/>
);
}
+35 -35
View File
@@ -1,51 +1,51 @@
export const crear = {
nombreFormulario:
'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán',
export const feriaSexualidad = {
nombre_form: 'Registro para la Feria de la Sexualidad - FES Acatlán',
descripcion:
'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
banner: 'base64',
fecha_inicio: '2025-03-07T00:00:01',
fecha_fin: '2025-03-18T23:59:59',
id_tipo_cuestionario: 1,
secciones: [
{
titulo: 'Participación en actividades',
titulo: 'Información Personal',
descripcion:
'Selecciona las actividades en las que te gustaría participar durante la Feria.',
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
preguntas: [
{
titulo: '¿Qué tipo de actividades te interesan?',
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
tipo: 'Multiple',
opciones: [
{ valor: 'Talleres sobre salud sexual' },
{ valor: 'Charlas informativas' },
{ valor: 'Mesas de diálogo y reflexión' },
],
opciones: [{ valor: 'Si' }, { valor: 'No' }],
obligatoria: true,
},
{
titulo:
'¿Estás interesado/a en recibir un certificado de participación? ',
tipo: 'Cerrada',
opciones: [{ valor: 'Si' }, { valor: 'No' }],
obligatoria: false,
},
],
},
{
titulo: 'Comentarios',
descripcion:
'Comparte tus ideas, inquietudes o sugerencias para esta y futuras ediciones',
preguntas: [
{
titulo:
'¿Hay algún tema específico que te gustaría que se abordara en la feria? ',
tipo: 'Abierta',
opciones: [],
obligatoria: false,
titulo: 'Numero de cuenta',
obligatoria: true,
tipo: 'Numero',
},
{
titulo: 'Comentarios adicionales',
tipo: 'Abierta',
opciones: [],
obligatoria: false,
titulo: 'Nombre(s)',
obligatoria: true,
tipo: 'Texto',
},
{
titulo: 'Apellidos',
obligatoria: true,
tipo: 'Texto',
},
{
titulo: 'genero',
obligatoria: true,
tipo: 'Radio',
opciones: [
{ valor: 'Masculino' },
{ valor: 'Femenino' },
{ valor: 'Prefiero no decirlo' },
],
},
{
titulo: 'Institución de procedencia',
tipo: 'Abierto',
obligatoria: true,
},
],
},
+5 -3
View File
@@ -5,9 +5,11 @@ export const res: CuestionarioCompleto = {
},
cuestionario: {
id_cuestionario: 10,
nombre_form: 'Encuesta de Satisfacción',
nombre_form:
'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán',
contador_secciones: 2,
descripcion: 'Encuesta para medir la satisfacción del cliente',
descripcion:
'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
editable: true,
fecha_inicio: '2025-03-26T00:00:00',
fecha_fin: '2025-03-31T23:59:59',
@@ -20,7 +22,7 @@ export const res: CuestionarioCompleto = {
seccion: {
id_seccion: 100,
contador_pregunta: 2,
descripcion: 'Preguntas generales sobre el servicio',
descripcion: 'Preguntas generales',
titulo: 'General',
},
preguntas: [
+22
View File
@@ -0,0 +1,22 @@
/* const comunidad = {
id_formulario: 1,
respuestas: [
{ id_pregunta: 101, valor: 'Si' }, // ¿Eres parte de la comunidad de la FES Acatlán?
{ id_pregunta: 102, valor: '123456789' }, // Numero de cuenta
{ id_pregunta: 106, valor: 'Prefiero no decirlo' },
],
fecha_envio: '2025-04-02T13:45:00',
};
const externos = {
id_formulario: 1,
respuestas: [
{ id_pregunta: 101, valor: 'No' }, // ¿Eres parte de la comunidad de la FES Acatlán?
{ id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s)
{ id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos
{ id_pregunta: 106, valor: 'Prefiero no decirlo' }, // genero
{ id_pregunta: 107, valor: 'UAM Azcapotzalco' }, // Institución de procedencia
],
fecha_envio: '2025-04-02T13:45:00',
};
*/
+11 -1
View File
@@ -5,7 +5,7 @@ $hover-scales: (
2: 1.1,
3: 1.15,
4: 1.2,
5: 1.25
5: 1.25,
);
@each $key, $value in $hover-scales {
@@ -17,3 +17,13 @@ $hover-scales: (
}
}
}
$min-width: 100px;
$max-width: 600px;
@for $i from 1 through (($max-width - $min-width) / 100px + 1) {
$current-width: $min-width + ($i - 1) * 100px;
.w-#{$current-width} {
width: $current-width;
}
}