300 lines
9.8 KiB
TypeScript
300 lines
9.8 KiB
TypeScript
|
import { createEffect, createSignal, Show } from "solid-js";
|
||
|
import { JSX } from "solid-js/jsx-runtime";
|
||
|
// import { Person } from "../../types/Person";
|
||
|
// import { RegisterPerson } from "./Search/RegisterPerson";
|
||
|
import QR from "qrcode";
|
||
|
import { CopyIcon } from "../icons/CopyIcon";
|
||
|
import { MagnifyingGlassIcon } from "../icons/MagnifyingGlassIcon";
|
||
|
import { XIcon } from "../icons/XIcon";
|
||
|
|
||
|
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
|
||
|
submitter: HTMLElement;
|
||
|
}>;
|
||
|
type HTMLButtonEvent = JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
|
||
|
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
type Person = any;
|
||
|
|
||
|
/*
|
||
|
Form that retrieves a user from the DB given an ID
|
||
|
*/
|
||
|
export function Search(props: {setPerson: (p: Person | null) => void}) {
|
||
|
const [dni, setDni] = createSignal("");
|
||
|
const [loading, setLoading] = createSignal(false);
|
||
|
const [error, setError] = createSignal("");
|
||
|
const [warning, setWarning] = createSignal("");
|
||
|
const [qrBase64, setQrBase64] = createSignal<string | null>(null);
|
||
|
const [persona, setPersona] = createSignal<Person | null>(null);
|
||
|
|
||
|
// Update QR
|
||
|
createEffect(() => {
|
||
|
if (dni() !== "") {
|
||
|
// Old URL: https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}
|
||
|
// New URL: https://eegsac.com/certificado/${dni()}
|
||
|
QR.toDataURL(`https://eegsac.com/certificado/${dni()}`, {margin: 1}, (err, res) => {
|
||
|
if (err) {
|
||
|
console.error("Error creating QR code");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
setQrBase64(res);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/*
|
||
|
Get the user data from the DB
|
||
|
*/
|
||
|
const searchDNI: HTMLEventFn = (ev) => {
|
||
|
ev.preventDefault();
|
||
|
search();
|
||
|
};
|
||
|
|
||
|
const search = async() => {
|
||
|
setLoading(true);
|
||
|
setError("");
|
||
|
setWarning("");
|
||
|
|
||
|
try {
|
||
|
const response = await fetch(`/person/${dni()}`);
|
||
|
const body = await response.json();
|
||
|
if (response.ok) {
|
||
|
setPersona(body);
|
||
|
props.setPerson(body);
|
||
|
} else if (response.status === 404) {
|
||
|
console.error(body);
|
||
|
|
||
|
setWarning("Persona no encontrada. Se debe insertar manualmente sus datos.");
|
||
|
props.setPerson(null);
|
||
|
} else {
|
||
|
setError(body);
|
||
|
}
|
||
|
} catch (e) {
|
||
|
setError(JSON.stringify(e));
|
||
|
}
|
||
|
|
||
|
setLoading(false);
|
||
|
};
|
||
|
|
||
|
const nombresYApellidos = () => {
|
||
|
const p = persona();
|
||
|
if (p === null) {
|
||
|
return "";
|
||
|
}
|
||
|
return `${p.nombres} ${p.apellidoPaterno} ${p.apellidoMaterno}`;
|
||
|
};
|
||
|
|
||
|
const apellidosYNombres = () => {
|
||
|
const p = persona();
|
||
|
if (p === null) {
|
||
|
return "";
|
||
|
}
|
||
|
return `${p.apellidoPaterno} ${p.apellidoMaterno} ${p.nombres}`;
|
||
|
};
|
||
|
|
||
|
const apellidos = () => {
|
||
|
const p = persona();
|
||
|
if (p === null) {
|
||
|
return "";
|
||
|
}
|
||
|
return `${p.apellidoPaterno} ${p.apellidoMaterno}`;
|
||
|
};
|
||
|
|
||
|
const nombres = () => {
|
||
|
const p = persona();
|
||
|
if (p === null) {
|
||
|
return "";
|
||
|
}
|
||
|
return p.nombres;
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<div>
|
||
|
<div class="text-center">
|
||
|
<div class="my-4 inline-block w-[10rem] h-[10rem] rounded-lg bg-c-surface-variant overflow-hidden">
|
||
|
<img class={`${qrBase64() === null ? "hidden" : ""} inline-block w-[10rem] h-[10rem]`} src={qrBase64() ?? ""} />
|
||
|
</div>
|
||
|
</div>
|
||
|
<div>
|
||
|
<form onSubmit={searchDNI} class="px-4">
|
||
|
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
|
||
|
</form>
|
||
|
|
||
|
<p
|
||
|
class="relative max-w-[14rem] mx-auto p-1 text-c-error text-sm"
|
||
|
style={{display: error() === "" ? "none" : "block"}}
|
||
|
>
|
||
|
Error:
|
||
|
<br />
|
||
|
{error()}
|
||
|
</p>
|
||
|
|
||
|
<br />
|
||
|
|
||
|
<div class={`${persona() === null ? "opacity-50 cursor-not-allowed" : ""}`}>
|
||
|
<MaterialLabel text={persona()?.apellidoPaterno ?? null} resource="Apellido Paterno" />
|
||
|
<MaterialLabel text={persona()?.apellidoMaterno ?? null} resource="Apellido Materno" />
|
||
|
<MaterialLabel text={persona()?.nombres ?? null} resource="Nombres" />
|
||
|
|
||
|
<div class={"relative max-w-[14rem] mx-auto my-6"}>
|
||
|
<CopyButton copyText={nombresYApellidos()}>
|
||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||
|
|
||
|
Nombres y <b>Apellidos</b>
|
||
|
</CopyButton>
|
||
|
|
||
|
<CopyButton copyText={apellidosYNombres()}>
|
||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||
|
|
||
|
<b>Apellidos</b> y Nombres
|
||
|
</CopyButton>
|
||
|
|
||
|
<div class="grid grid-cols-2 gap-2">
|
||
|
<CopyButton copyText={apellidos()}>
|
||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||
|
|
||
|
<b>Apellidos</b>
|
||
|
</CopyButton>
|
||
|
<CopyButton copyText={nombres()}>
|
||
|
<CopyIcon fill="var(--c-on-primary)" />
|
||
|
|
||
|
Nombres
|
||
|
</CopyButton>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<Show when={warning() !== ""}>
|
||
|
{/*
|
||
|
<RegisterPerson dni={dni()} onSuccess={search} />
|
||
|
*/}
|
||
|
</Show>
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function InputBox(props: {
|
||
|
loading: boolean,
|
||
|
dni: string,
|
||
|
setDni: (v: string) => void,
|
||
|
}) {
|
||
|
const inputElement = (
|
||
|
<input
|
||
|
id="search-dni"
|
||
|
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1 w-full
|
||
|
invalid:border-c-error invalid:text-c-error
|
||
|
focus:border-c-primary outline-none font-mono
|
||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||
|
type="text"
|
||
|
minLength={8}
|
||
|
maxLength={8}
|
||
|
pattern="[0-9]{8}"
|
||
|
placeholder="Número de DNI"
|
||
|
value={props.dni}
|
||
|
required
|
||
|
onChange={(e) => props.setDni(e.target.value)}
|
||
|
disabled={props.loading}
|
||
|
/>
|
||
|
);
|
||
|
|
||
|
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
||
|
ev.preventDefault();
|
||
|
|
||
|
if (props.dni.length === 8) {
|
||
|
navigator.clipboard.writeText(props.dni);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const clearDni: HTMLButtonEvent = (ev) => {
|
||
|
ev.preventDefault();
|
||
|
|
||
|
props.setDni("");
|
||
|
(inputElement as HTMLInputElement).focus();
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<div class="relative max-w-[14rem] mx-auto">
|
||
|
{inputElement}
|
||
|
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">DNI</label>
|
||
|
<button
|
||
|
class="absolute top-1 right-[3.75rem] rounded hover:bg-c-surface-variant"
|
||
|
>
|
||
|
<MagnifyingGlassIcon fill="var(--c-on-surface)" />
|
||
|
</button>
|
||
|
<button
|
||
|
type="button"
|
||
|
class="absolute top-1 right-8 rounded hover:bg-c-surface-variant"
|
||
|
onclick={copyToClipboard}
|
||
|
>
|
||
|
<CopyIcon fill="var(--c-on-surface)" />
|
||
|
</button>
|
||
|
<button
|
||
|
type="button"
|
||
|
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
||
|
onclick={clearDni}
|
||
|
>
|
||
|
<XIcon fill="var(--c-on-surface)" />
|
||
|
</button>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
|
||
|
function MaterialLabel(props: {text: string | null, resource: string}) {
|
||
|
const copyToClipboard: HTMLButtonEvent = (ev) => {
|
||
|
ev.preventDefault();
|
||
|
|
||
|
if (props.text !== null) {
|
||
|
navigator.clipboard.writeText(props.text);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<div class="relative max-w-[14rem] mx-auto my-6">
|
||
|
<label for="search-dni" class="absolute -top-2 left-2 text-xs bg-c-surface px-1 select-none">{props.resource}</label>
|
||
|
<span
|
||
|
class="bg-c-background text-c-on-background border-c-outline
|
||
|
border-2 rounded px-2 py-1 w-full inline-block font-mono
|
||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||
|
>
|
||
|
{props.text ?? ""}
|
||
|
<span class="select-none"> </span>
|
||
|
</span>
|
||
|
|
||
|
<button
|
||
|
type="button"
|
||
|
class="absolute top-1 right-1 rounded hover:bg-c-surface-variant"
|
||
|
onclick={copyToClipboard}
|
||
|
>
|
||
|
<CopyIcon fill="var(--c-on-surface)" />
|
||
|
</button>
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function CopyButton(props: {copyText: string, children: Array<JSX.Element> | JSX.Element}) {
|
||
|
const [successAnimation, setSuccessAnimation] = createSignal(false);
|
||
|
|
||
|
const onclick = () => {
|
||
|
if (props.copyText !== "") {
|
||
|
navigator.clipboard.writeText(props.copyText);
|
||
|
setSuccessAnimation(true);
|
||
|
setTimeout(() => setSuccessAnimation(false), 500);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<button
|
||
|
onclick={onclick}
|
||
|
class={
|
||
|
`${successAnimation() ? "bg-c-success text-c-on-success" : "bg-c-primary text-c-on-primary"
|
||
|
} rounded-lg transition-colors py-1 my-1 relative overflow-hidden inline-block w-full`
|
||
|
}
|
||
|
>
|
||
|
{props.children}
|
||
|
</button>
|
||
|
);
|
||
|
}
|
||
|
|