Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8fe625e8 |
@ -1,4 +1,4 @@
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = {
|
||||
production: {
|
||||
@ -12,11 +12,12 @@ module.exports = {
|
||||
seederStorage: 'sequelize',
|
||||
},
|
||||
development: {
|
||||
username: 'postgres',
|
||||
username: process.env.DB_USER || 'postgres',
|
||||
dialect: 'postgres',
|
||||
password: '',
|
||||
database: 'db_renacer_certificados_saas',
|
||||
password: process.env.DB_PASS || '',
|
||||
database: process.env.DB_NAME || 'db_renacer_certificados_saas',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
logging: console.log,
|
||||
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/certificates', passport.authenticate('jwt', {session: false}), certificatesRoutes);
|
||||
app.use('/api/certificates', certificatesRoutes);
|
||||
|
||||
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 appTitle = 'created by Flatlogic generator!'
|
||||
export const appTitle = 'Corporación Renacer - DDHH Mundiales'
|
||||
|
||||
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
||||
|
||||
|
||||
@ -1,166 +1,157 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import { useRouter } from 'next/router';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
import { mdiShieldCheck, mdiAccountGroup, mdiCertificate, mdiMagnify } from '@mdi/js';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
|
||||
export default function LandingPage() {
|
||||
const router = useRouter();
|
||||
const [searchCode, setSearchCode] = useState('');
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
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>)
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (searchCode.trim()) {
|
||||
router.push(`/verify?code=${searchCode.trim()}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
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',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div className="min-h-screen bg-white text-gray-900">
|
||||
<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>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{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>
|
||||
{/* Navigation */}
|
||||
<nav className="flex items-center justify-between p-6 lg:px-8 border-b" aria-label="Global">
|
||||
<div className="flex lg:flex-1">
|
||||
<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>
|
||||
|
||||
<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
|
||||
<span className="text-xl font-bold tracking-tight text-blue-900 uppercase">Renacer DDHH</span>
|
||||
</Link>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
LandingPage.getLayout = function getLayout(page: ReactElement) {
|
||||
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