@@ -1,23 +1,37 @@
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { publicClient } from '../../api/publicClient'
|
||||
import type { CertificateVerification } from '../../api/types'
|
||||
import { Icon } from '../../components/Icon'
|
||||
import { PublicLayout } from './PublicLayout'
|
||||
import { Button, Input } from '../../components/ui'
|
||||
import { AcademyLayout } from '../../academy/AcademyLayout'
|
||||
import { formatLocal } from '../../lib/dates'
|
||||
|
||||
/**
|
||||
* Weryfikacja certyfikatu.
|
||||
*
|
||||
* DO CZEGO SLUZY
|
||||
* ------------------------------------------------------------------------
|
||||
* To jest PUBLICZNA weryfikacja po numerze, dla osoby z zewnatrz (pracodawca,
|
||||
* instytucja), ktora dostala dokument i chce potwierdzic jego autentycznosc.
|
||||
* Celowo bez logowania: sprawdzajacy nie ma u nas konta, a numer widniejacy
|
||||
* na dokumencie sluzy wlasnie do takiego sprawdzenia. To NIE jest miejsce,
|
||||
* gdzie kursant oglada wlasne certyfikaty - te sa na pulpicie po zalogowaniu.
|
||||
*
|
||||
* CO POKAZUJEMY, A CZEGO NIE
|
||||
* ------------------------------------------------------------------------
|
||||
* Strona jest otwarta dla kazdego, kto zna numer, wiec pokazuje **wylacznie**
|
||||
* to, co potrzebne do potwierdzenia autentycznosci: numer, tytul, date, imie
|
||||
* z pierwsza litera nazwiska i informacje o waznosci.
|
||||
* z pierwsza litera nazwiska i informacje o waznosci. Nie ma tu pelnego
|
||||
* nazwiska, adresu e-mail ani mozliwosci pobrania pliku.
|
||||
*
|
||||
* Nie ma tu pelnego nazwiska, adresu e-mail ani mozliwosci pobrania pliku.
|
||||
* Pracodawca sprawdzajacy dokument ma potwierdzic, ze jest prawdziwy, a nie
|
||||
* poznac dane osoby, ktorej nie zna, ani pobrac cudzy certyfikat.
|
||||
* WEJSCIE BEZ NUMERU POKAZUJE FORMULARZ, NIE BLAD
|
||||
* ------------------------------------------------------------------------
|
||||
* Trasa `/certyfikat/*` lapie takze gole `/certyfikat` (ze stopki). Bez numeru
|
||||
* nie ma czego weryfikowac, wiec zamiast odpytywac API o pusty numer i straszyc
|
||||
* czerwonym "nie znaleziono", pokazujemy pole do wpisania numeru. Komunikat
|
||||
* bledu pojawia sie dopiero po realnym wpisaniu numeru, ktory nie istnieje.
|
||||
*/
|
||||
export function CertificateVerifyPage() {
|
||||
/*
|
||||
@@ -26,8 +40,97 @@ export function CertificateVerifyPage() {
|
||||
* by sie nie dopasowala. `*` lapie cala reszte sciezki wraz z ukosnikami.
|
||||
*/
|
||||
const params = useParams()
|
||||
const number = decodeURIComponent(params['*'] ?? '')
|
||||
const number = decodeURIComponent(params['*'] ?? '').trim()
|
||||
|
||||
return (
|
||||
<AcademyLayout>
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-slate-500 transition-colors duration-fast hover:text-slate-900"
|
||||
>
|
||||
<Icon name="arrow-left" size={16} />
|
||||
Strona główna
|
||||
</Link>
|
||||
|
||||
{number ? <VerifyResult number={number} /> : <VerifyForm />}
|
||||
</div>
|
||||
</AcademyLayout>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formularz wpisania numeru.
|
||||
*
|
||||
* Po wyslaniu przechodzimy na `/certyfikat/{numer}`: numer trafia do adresu,
|
||||
* wiec wynik da sie odswiezyc, dodac do zakladek i podeslac linkiem - tak samo
|
||||
* jak przy wejsciu z linku na dokumencie.
|
||||
*/
|
||||
function VerifyForm() {
|
||||
const navigate = useNavigate()
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
const submit = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return
|
||||
// numer moze zawierac ukosnik ("2026/1") - trasa z wildcardem to obsluguje,
|
||||
// ale poszczegolne segmenty koduje sie, zeby znaki specjalne nie rozbily URL
|
||||
const encoded = trimmed.split('/').map(encodeURIComponent).join('/')
|
||||
navigate(`/certyfikat/${encoded}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-slate-200 bg-white p-6">
|
||||
<div className="text-center">
|
||||
<Icon name="award" size={36} className="mx-auto text-blue-500" />
|
||||
<h1 className="mt-3 text-lg font-semibold text-slate-900">Weryfikacja certyfikatu</h1>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Wpisz numer widniejący na dokumencie, aby sprawdzić jego autentyczność.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={submit} className="mx-auto mt-6 flex max-w-md flex-col gap-3 sm:flex-row">
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder="np. 2026/1"
|
||||
aria-label="Numer certyfikatu"
|
||||
className="hx-data flex-1"
|
||||
autoFocus
|
||||
/>
|
||||
<Button type="submit" icon="search" disabled={!value.trim()}>
|
||||
Sprawdź
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/*
|
||||
Skad wziac numer. Sprawdzajacy z zewnatrz (pracodawca) trzyma dokument
|
||||
w reku i moze nie wiedziec, ktora liczba na nim to numer do weryfikacji.
|
||||
*/}
|
||||
<div className="mx-auto mt-6 max-w-md rounded-lg bg-slate-50 p-4 text-sm text-slate-600">
|
||||
<p className="flex items-center gap-2 font-medium text-slate-700">
|
||||
<Icon name="help-circle" size={16} className="text-slate-400" />
|
||||
Skąd wziąć numer certyfikatu?
|
||||
</p>
|
||||
<p className="mt-2 leading-relaxed">
|
||||
Numer widnieje na samym dokumencie (PDF lub wydruku), zwykle w formacie
|
||||
<span className="hx-data"> rok/numer</span>, na przykład
|
||||
<span className="hx-data"> 2026/1</span>. Znajdziesz go w treści
|
||||
certyfikatu, przy dacie wystawienia. Przepisz go dokładnie, razem
|
||||
z ukośnikiem.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-5 border-t border-slate-100 pt-4 text-center text-xs text-slate-500">
|
||||
Chcesz zobaczyć własne certyfikaty? Znajdziesz je po zalogowaniu na swoim koncie.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/** Wynik weryfikacji dla podanego numeru. */
|
||||
function VerifyResult({ number }: { number: string }) {
|
||||
const query = useQuery({
|
||||
queryKey: ['certificate-verify', number],
|
||||
queryFn: async () => {
|
||||
@@ -42,71 +145,72 @@ export function CertificateVerifyPage() {
|
||||
|
||||
if (query.isPending) {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<div className="flex justify-center py-16">
|
||||
<Icon name="loader" size={28} className="animate-spin text-slate-400" />
|
||||
</div>
|
||||
</PublicLayout>
|
||||
<div className="flex justify-center py-16">
|
||||
<Icon name="loader" size={28} className="animate-spin text-slate-400" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (query.isError) {
|
||||
return (
|
||||
<PublicLayout>
|
||||
<section className="rounded-xl border border-slate-200 bg-white p-6 text-center">
|
||||
<Icon name="x-circle" size={36} className="mx-auto text-alarm-500" />
|
||||
<h1 className="mt-3 text-lg font-semibold text-slate-900">
|
||||
Nie znaleźliśmy takiego certyfikatu
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Sprawdź, czy numer został przepisany dokładnie tak, jak widnieje na dokumencie.
|
||||
</p>
|
||||
</section>
|
||||
</PublicLayout>
|
||||
<section className="rounded-xl border border-slate-200 bg-white p-6 text-center">
|
||||
<Icon name="x-circle" size={36} className="mx-auto text-alarm-500" />
|
||||
<h1 className="mt-3 text-lg font-semibold text-slate-900">
|
||||
Nie znaleźliśmy takiego certyfikatu
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Sprawdź, czy numer został przepisany dokładnie tak, jak widnieje na dokumencie.
|
||||
</p>
|
||||
<div className="mt-5">
|
||||
<Link to="/certyfikat">
|
||||
<Button variant="secondary" size="sm" icon="arrow-left">
|
||||
Wpisz numer ponownie
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const data = query.data
|
||||
|
||||
return (
|
||||
<PublicLayout>
|
||||
<section className="rounded-xl border border-slate-200 bg-white p-6">
|
||||
<div className="text-center">
|
||||
{data.valid ? (
|
||||
<>
|
||||
<Icon name="check-circle" size={40} className="mx-auto text-ok-500" />
|
||||
<h1 className="mt-3 text-lg font-semibold text-slate-900">Certyfikat jest ważny</h1>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="x-circle" size={40} className="mx-auto text-alarm-500" />
|
||||
<h1 className="mt-3 text-lg font-semibold text-slate-900">
|
||||
Certyfikat został unieważniony
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Dokument o tym numerze istnieje, ale wystawca wycofał go z obiegu.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<section className="rounded-xl border border-slate-200 bg-white p-6">
|
||||
<div className="text-center">
|
||||
{data.valid ? (
|
||||
<>
|
||||
<Icon name="check-circle" size={40} className="mx-auto text-ok-500" />
|
||||
<h1 className="mt-3 text-lg font-semibold text-slate-900">Certyfikat jest ważny</h1>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="x-circle" size={40} className="mx-auto text-alarm-500" />
|
||||
<h1 className="mt-3 text-lg font-semibold text-slate-900">
|
||||
Certyfikat został unieważniony
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Dokument o tym numerze istnieje, ale wystawca wycofał go z obiegu.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<dl className="mt-6 divide-y divide-slate-100 border-t border-slate-100">
|
||||
<Row label="Numer certyfikatu" value={data.number} mono />
|
||||
<Row label={data.forCourse ? 'Kurs' : 'Szkolenie'} value={data.subjectTitle} />
|
||||
<Row label="Uczestnik" value={data.participantName} />
|
||||
<Row label="Data wystawienia" value={formatLocal(data.issuedAt)} mono />
|
||||
</dl>
|
||||
<dl className="mt-6 divide-y divide-slate-100 border-t border-slate-100">
|
||||
<Row label="Numer certyfikatu" value={data.number} mono />
|
||||
<Row label={data.forCourse ? 'Kurs' : 'Szkolenie'} value={data.subjectTitle} />
|
||||
<Row label="Uczestnik" value={data.participantName} />
|
||||
<Row label="Data wystawienia" value={formatLocal(data.issuedAt)} mono />
|
||||
</dl>
|
||||
|
||||
{/*
|
||||
Jawne powiedzenie, czego tu nie ma. Sprawdzajacy widzi skrocone imie
|
||||
i moze uznac to za blad; to jest decyzja, nie usterka.
|
||||
*/}
|
||||
<p className="mt-5 border-t border-slate-100 pt-4 text-xs text-slate-500">
|
||||
Ze względu na ochronę danych osobowych pokazujemy imię i pierwszą literę nazwiska.
|
||||
Pełne dane znajdują się na dokumencie, który posiada uczestnik.
|
||||
</p>
|
||||
</section>
|
||||
</PublicLayout>
|
||||
{/*
|
||||
Jawne powiedzenie, czego tu nie ma. Sprawdzajacy widzi skrocone imie
|
||||
i moze uznac to za blad; to jest decyzja, nie usterka.
|
||||
*/}
|
||||
<p className="mt-5 border-t border-slate-100 pt-4 text-xs text-slate-500">
|
||||
Ze względu na ochronę danych osobowych pokazujemy imię i pierwszą literę nazwiska.
|
||||
Pełne dane znajdują się na dokumencie, który posiada uczestnik.
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user