Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8fe625e8 |
@ -1,4 +1,4 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
production: {
|
production: {
|
||||||
@ -12,11 +12,12 @@ module.exports = {
|
|||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
},
|
},
|
||||||
development: {
|
development: {
|
||||||
username: 'postgres',
|
username: process.env.DB_USER || 'postgres',
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
password: '',
|
password: process.env.DB_PASS || '',
|
||||||
database: 'db_renacer_certificados_saas',
|
database: process.env.DB_NAME || 'db_renacer_certificados_saas',
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: process.env.DB_PORT || 5432,
|
||||||
logging: console.log,
|
logging: console.log,
|
||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
},
|
},
|
||||||
|
|||||||
45
backend/src/db/migrations/1772556991799.js
Normal file
45
backend/src/db/migrations/1772556991799.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
const { v4: uuid } = require('uuid');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
const createdAt = new Date();
|
||||||
|
const updatedAt = new Date();
|
||||||
|
|
||||||
|
// Get the Public role ID
|
||||||
|
const [roles] = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id FROM "roles" WHERE name = 'Public' LIMIT 1`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!roles || roles.length === 0) {
|
||||||
|
console.error("Public role not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicRoleId = roles[0].id;
|
||||||
|
|
||||||
|
// Get permission IDs
|
||||||
|
const [permissions] = await queryInterface.sequelize.query(
|
||||||
|
`SELECT id, name FROM "permissions" WHERE name IN ('READ_CERTIFICATES', 'READ_USERS', 'READ_COMMUNITIES')`
|
||||||
|
);
|
||||||
|
|
||||||
|
const rolePermissions = permissions.map(p => ({
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
roles_permissionsId: publicRoleId,
|
||||||
|
permissionId: p.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Use ignoreDuplicates to avoid errors if already present
|
||||||
|
for (const rp of rolePermissions) {
|
||||||
|
await queryInterface.sequelize.query(
|
||||||
|
`INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
|
||||||
|
VALUES ('${rp.createdAt.toISOString()}', '${rp.updatedAt.toISOString()}', '${rp.roles_permissionsId}', '${rp.permissionId}')
|
||||||
|
ON CONFLICT DO NOTHING`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
// Revert logic
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -121,7 +121,7 @@ app.use('/api/organization_settings', passport.authenticate('jwt', {session: fal
|
|||||||
|
|
||||||
app.use('/api/certificate_templates', passport.authenticate('jwt', {session: false}), certificate_templatesRoutes);
|
app.use('/api/certificate_templates', passport.authenticate('jwt', {session: false}), certificate_templatesRoutes);
|
||||||
|
|
||||||
app.use('/api/certificates', passport.authenticate('jwt', {session: false}), certificatesRoutes);
|
app.use('/api/certificates', certificatesRoutes);
|
||||||
|
|
||||||
app.use('/api/payments_log', passport.authenticate('jwt', {session: false}), payments_logRoutes);
|
app.use('/api/payments_log', passport.authenticate('jwt', {session: false}), payments_logRoutes);
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
|
|||||||
|
|
||||||
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
||||||
|
|
||||||
export const appTitle = 'created by Flatlogic generator!'
|
export const appTitle = 'Corporación Renacer - DDHH Mundiales'
|
||||||
|
|
||||||
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
||||||
|
|
||||||
|
|||||||
@ -1,166 +1,157 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import { useRouter } from 'next/router';
|
||||||
import CardBox from '../components/CardBox';
|
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import { mdiShieldCheck, mdiAccountGroup, mdiCertificate, mdiMagnify } from '@mdi/js';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [searchCode, setSearchCode] = useState('');
|
||||||
|
|
||||||
export default function Starter() {
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
e.preventDefault();
|
||||||
src: undefined,
|
if (searchCode.trim()) {
|
||||||
photographer: undefined,
|
router.push(`/verify?code=${searchCode.trim()}`);
|
||||||
photographer_url: undefined,
|
|
||||||
})
|
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
|
||||||
const [contentType, setContentType] = useState('video');
|
|
||||||
const [contentPosition, setContentPosition] = useState('right');
|
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
|
||||||
|
|
||||||
const title = 'Renacer Certificados SaaS'
|
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
const image = await getPexelsImage();
|
|
||||||
const video = await getPexelsVideo();
|
|
||||||
setIllustrationImage(image);
|
|
||||||
setIllustrationVideo(video);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
|
||||||
<div
|
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
|
||||||
style={{
|
|
||||||
backgroundImage: `${
|
|
||||||
image
|
|
||||||
? `url(${image?.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={image?.photographer_url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Photo by {image?.photographer} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
|
||||||
if (video?.video_files?.length > 0) {
|
|
||||||
return (
|
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
|
||||||
<video
|
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
>
|
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={video?.user?.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Video by {video.user.name} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="min-h-screen bg-white text-gray-900">
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('Inicio')}</title>
|
||||||
|
<meta name="description" content="Corporación Renacer DDHH Mundiales - Plataforma de Certificación de Líderes Sociales" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
{/* Navigation */}
|
||||||
<div
|
<nav className="flex items-center justify-between p-6 lg:px-8 border-b" aria-label="Global">
|
||||||
className={`flex ${
|
<div className="flex lg:flex-1">
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<Link href="/" className="-m-1.5 p-1.5 flex items-center gap-2">
|
||||||
} min-h-screen w-full`}
|
<div className="bg-blue-900 p-2 rounded-lg text-white">
|
||||||
>
|
<BaseIcon path={mdiShieldCheck} size={24} />
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
|
||||||
? imageBlock(illustrationImage)
|
|
||||||
: null}
|
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
|
||||||
? videoBlock(illustrationVideo)
|
|
||||||
: null}
|
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
|
||||||
<CardBoxComponentTitle title="Welcome to your Renacer Certificados SaaS app!"/>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
|
||||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-xl font-bold tracking-tight text-blue-900 uppercase">Renacer DDHH</span>
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton
|
|
||||||
href='/login'
|
|
||||||
label='Login'
|
|
||||||
color='info'
|
|
||||||
className='w-full'
|
|
||||||
/>
|
|
||||||
|
|
||||||
</BaseButtons>
|
|
||||||
</CardBox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionFullScreen>
|
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-x-6">
|
||||||
|
<Link href="/login" className="text-sm font-semibold leading-6 text-gray-900">
|
||||||
|
Acceso Administrativo
|
||||||
|
</Link>
|
||||||
|
<Link href="/register" className="text-sm font-semibold leading-6 text-blue-900 border border-blue-900 px-4 py-1 rounded-md hover:bg-blue-50">
|
||||||
|
Registrarse como Líder
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="relative isolate px-6 pt-14 lg:px-8 bg-gradient-to-b from-blue-50 to-white">
|
||||||
|
<div className="mx-auto max-w-2xl py-24 sm:py-32">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
|
||||||
|
Empoderando Líderes Sociales en Defensa de los Derechos Humanos
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 text-lg leading-8 text-gray-600">
|
||||||
|
Validamos y certificamos la labor de quienes transforman comunidades.
|
||||||
|
Garantizamos transparencia, verificabilidad y credibilidad internacional.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Verification Box */}
|
||||||
|
<div className="mt-10 flex flex-col items-center justify-center gap-y-4">
|
||||||
|
<form onSubmit={handleSearch} className="w-full max-w-md flex items-center bg-white rounded-full shadow-lg border p-1 focus-within:ring-2 focus-within:ring-blue-500 transition-all">
|
||||||
|
<div className="pl-4 text-gray-400">
|
||||||
|
<BaseIcon path={mdiMagnify} size={24} />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Ingrese código de certificado..."
|
||||||
|
className="flex-grow px-4 py-3 outline-none rounded-full text-gray-700"
|
||||||
|
value={searchCode}
|
||||||
|
onChange={(e) => setSearchCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="bg-blue-900 text-white px-6 py-3 rounded-full font-bold hover:bg-blue-800 transition-colors">
|
||||||
|
Verificar
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<p className="text-sm text-gray-500">¿Tienes un código QR? Escanéalo para verificación instantánea.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="py-24 sm:py-32 bg-white">
|
||||||
|
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-2xl lg:text-center">
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-blue-900 uppercase tracking-widest">Nuestra Misión</h2>
|
||||||
|
<p className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||||
|
Credibilidad Digital para el Impacto Social
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-none">
|
||||||
|
<dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none lg:grid-cols-3">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-900 text-white">
|
||||||
|
<BaseIcon path={mdiCertificate} size={32} />
|
||||||
|
</div>
|
||||||
|
<dt className="text-xl font-bold leading-7 text-gray-900">Certificación Oficial</dt>
|
||||||
|
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600">
|
||||||
|
<p>Emitimos certificados digitales con validez anual, respaldando la formación y trayectoria de líderes en DDHH.</p>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-900 text-white">
|
||||||
|
<BaseIcon path={mdiShieldCheck} size={32} />
|
||||||
|
</div>
|
||||||
|
<dt className="text-xl font-bold leading-7 text-gray-900">Verificabilidad Inmediata</dt>
|
||||||
|
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600">
|
||||||
|
<p>Cualquier entidad puede validar la autenticidad de un certificado mediante QR o código único en tiempo real.</p>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-900 text-white">
|
||||||
|
<BaseIcon path={mdiAccountGroup} size={32} />
|
||||||
|
</div>
|
||||||
|
<dt className="text-xl font-bold leading-7 text-gray-900">Comunidad de Líderes</dt>
|
||||||
|
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-gray-600">
|
||||||
|
<p>Más que un papel, es el acceso a una red global comprometida con la paz y la justicia social en Colombia y el mundo.</p>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-gray-900 text-white py-12">
|
||||||
|
<div className="mx-auto max-w-7xl px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<span className="text-2xl font-bold text-white tracking-widest uppercase">Renacer DDHH</span>
|
||||||
|
<p className="text-gray-400 text-sm max-w-xs">Corporación Renacer Derechos Humanos Mundiales. NIT: 9018960153. Sede Principal: Colombia.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-x-12">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h4 className="font-bold text-amber-400">Institucional</h4>
|
||||||
|
<Link href="#" className="text-sm text-gray-400 hover:text-white">Sobre Nosotros</Link>
|
||||||
|
<Link href="#" className="text-sm text-gray-400 hover:text-white">Misión y Visión</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h4 className="font-bold text-amber-400">Legal</h4>
|
||||||
|
<Link href="/terms-of-use" className="text-sm text-gray-400 hover:text-white">Términos</Link>
|
||||||
|
<Link href="/privacy-policy" className="text-sm text-gray-400 hover:text-white">Privacidad</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 border-t border-gray-800 pt-8 text-center text-sm text-gray-500">
|
||||||
|
© 2026 Corporación Renacer DDHH Mundiales. Todos los derechos reservados.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
LandingPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
165
frontend/src/pages/verify.tsx
Normal file
165
frontend/src/pages/verify.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import type { ReactElement } from 'react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import axios from 'axios';
|
||||||
|
import LayoutGuest from '../layouts/Guest';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import { mdiShieldCheck, mdiAlertCircle, mdiCheckDecagram, mdiClockOutline, mdiCloseOctagon } from '@mdi/js';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function VerificationPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { code } = router.query;
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [certificate, setCertificate] = useState<any>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (code) {
|
||||||
|
fetchCertificate(code as string);
|
||||||
|
} else if (router.isReady && !code) {
|
||||||
|
setLoading(false);
|
||||||
|
setError('No se proporcionó un código de verificación.');
|
||||||
|
}
|
||||||
|
}, [code, router.isReady]);
|
||||||
|
|
||||||
|
const fetchCertificate = async (searchCode: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
// Search by code
|
||||||
|
const response = await axios.get(`/certificates?certificate_code=${searchCode}`);
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (data.rows && data.rows.length > 0) {
|
||||||
|
setCertificate(data.rows[0]);
|
||||||
|
} else {
|
||||||
|
setError('Certificado no encontrado. Verifique el código e intente nuevamente.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('Error al conectar con el servidor de validación.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusInfo = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'ACTIVO':
|
||||||
|
return { color: 'text-green-600', bg: 'bg-green-100', icon: mdiCheckDecagram, label: 'VÁLIDO / ACTIVO' };
|
||||||
|
case 'VENCIDO':
|
||||||
|
return { color: 'text-amber-600', bg: 'bg-amber-100', icon: mdiClockOutline, label: 'CADUCADO' };
|
||||||
|
case 'REVOCADO':
|
||||||
|
return { color: 'text-red-600', bg: 'bg-red-100', icon: mdiCloseOctagon, label: 'REVOCADO / ANULADO' };
|
||||||
|
default:
|
||||||
|
return { color: 'text-gray-600', bg: 'bg-gray-100', icon: mdiAlertCircle, label: 'DESCONOCIDO' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-50 flex flex-col">
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Verificación de Certificado')}</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<nav className="flex items-center justify-between p-6 lg:px-8 border-b bg-white shadow-sm">
|
||||||
|
<Link href="/" className="-m-1.5 p-1.5 flex items-center gap-2">
|
||||||
|
<div className="bg-blue-900 p-2 rounded-lg text-white">
|
||||||
|
<BaseIcon path={mdiShieldCheck} size={24} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-bold tracking-tight text-blue-900 uppercase">Renacer DDHH</span>
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main className="flex-grow flex items-center justify-center p-6 text-gray-900">
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="animate-spin inline-block w-8 h-8 border-4 border-blue-900 border-t-transparent rounded-full mb-4"></div>
|
||||||
|
<p className="text-gray-600">Validando autenticidad en el registro oficial...</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="bg-white p-8 rounded-2xl shadow-xl border border-red-100 text-center">
|
||||||
|
<div className="text-red-500 mb-4 flex justify-center">
|
||||||
|
<BaseIcon path={mdiAlertCircle} size={64} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Validación Fallida</h2>
|
||||||
|
<p className="text-gray-600 mb-8">{error}</p>
|
||||||
|
<Link href="/" className="bg-blue-900 text-white px-8 py-3 rounded-full font-bold hover:bg-blue-800 transition-colors">
|
||||||
|
Volver al Inicio
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : certificate && (
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl overflow-hidden border border-blue-100">
|
||||||
|
<div className={`p-8 ${getStatusInfo(certificate.status).bg} flex flex-col items-center text-center`}>
|
||||||
|
<div className={`${getStatusInfo(certificate.status).color} mb-4`}>
|
||||||
|
<BaseIcon path={getStatusInfo(certificate.status).icon} size={80} />
|
||||||
|
</div>
|
||||||
|
<h2 className={`text-3xl font-black ${getStatusInfo(certificate.status).color}`}>
|
||||||
|
{getStatusInfo(certificate.status).label}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 mt-2 font-medium tracking-widest uppercase text-xs">Sello de Verificación Digital</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-8 space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Líder Certificado</h4>
|
||||||
|
<p className="text-xl font-bold text-gray-900">
|
||||||
|
{certificate.leader?.firstName} {certificate.leader?.lastName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Tipo de Credencial</h4>
|
||||||
|
<p className="text-xl font-bold text-gray-900">{certificate.title}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Código de Registro</h4>
|
||||||
|
<p className="text-lg font-mono font-bold text-blue-900 bg-blue-50 px-2 py-1 rounded inline-block uppercase">
|
||||||
|
{certificate.certificate_code}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Fecha de Emisión</h4>
|
||||||
|
<p className="text-lg font-bold text-gray-900">
|
||||||
|
{new Date(certificate.issue_date).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-6">
|
||||||
|
<h4 className="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2">Descripción de Aval</h4>
|
||||||
|
<p className="text-gray-700 leading-relaxed italic">
|
||||||
|
"{certificate.description}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-50 p-4 rounded-lg flex items-center gap-4 text-sm text-gray-500">
|
||||||
|
<BaseIcon path={mdiShieldCheck} size={24} className="text-blue-900" />
|
||||||
|
<p>Esta certificación ha sido emitida por la Corporación Renacer DDHH Mundiales y su integridad está garantizada mediante firma digital HMAC-SHA256.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-50 border-t flex justify-center">
|
||||||
|
<Link href="/" className="text-blue-900 font-bold hover:underline">
|
||||||
|
Nueva Consulta de Verificación
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-gray-900 text-white py-8 text-center text-sm">
|
||||||
|
<p>© 2026 Corporación Renacer DDHH Mundiales. NIT: 9018960153.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VerificationPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user