Compare commits

..

10 Commits

31 changed files with 893 additions and 10636 deletions

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ import {
AlignmentType,
BorderStyle,
} from "docx";
import { cmText, createSimpleText, createSimpleTextP, getImage, getQR } from "./utils";
import { cmText, createSimpleTextP, getImage, getQR } from "./utils";
import { CertData } from "./CertData";
const imgFondoDoc = getImage({
@ -30,7 +30,7 @@ const imgEATE = getImage({
height: 2.28,
width: 3.39,
horizontalOffset: 0.94,
verticalOffset: 13.77,
verticalOffset: 15.67,
});
const imgMTC = getImage({
@ -38,7 +38,7 @@ const imgMTC = getImage({
height: 1.6,
width: 5,
horizontalOffset: 0.9,
verticalOffset: 17.22,
verticalOffset: 18.3,
});
const imgOSHA = getImage({
@ -73,7 +73,7 @@ const tCertificate = createSimpleTextP({
// Se expide el presente
const tLabel3 = createSimpleTextP({
xPosition: 11.08,
yPosition: 4.62,
yPosition: 5.43,
width: 7.74,
height: 0.5,
text: "Se expide el presente a:",
@ -84,7 +84,7 @@ const tLabel3 = createSimpleTextP({
// SUPERVISOR ESCOLTA MATPEL
const tCourse = createSimpleTextP({
xPosition: 7.28,
yPosition: 7.94,
yPosition: 8.75,
width: 15.42,
height: 1.5,
text: "SUPERVISOR ESCOLTA MATPEL",
@ -95,7 +95,7 @@ const tCourse = createSimpleTextP({
// Por haber aprobado la dormacion...
const tLabel2 = createSimpleTextP({
xPosition: 10.93,
yPosition: 7.14,
yPosition: 7.95,
width: 7.74,
height: 0.6,
text: "Por haber aprobado la formación en el curso",
@ -106,7 +106,7 @@ const tLabel2 = createSimpleTextP({
// Temas tratados...
const tTopics = createSimpleTextP({
xPosition: 5.04,
yPosition: 9.49,
yPosition: 10.3,
width: 19.34,
height: 1.5,
text: "Temas tratados: Inspección de Unidades y Llenado de Herramientas de Gestión Check List, IPERC, Inspección básica de Kit de Emergencias, Parada de Controles e Inspección, Delimitación y Evaluación del Evento, Sistemas de Comando de Incidentes, Practicas Reales en caso de Derrames de MATPEL con Trajes, Evacuación de Pacientes Afectados con Camioneta, Primeros Auxilios básico, Con una duración de 36 horas lectivas.",
@ -118,7 +118,7 @@ const tTopics = createSimpleTextP({
// Respaldado por:
const tHours = createSimpleTextP({
xPosition: 1.15,
yPosition: 13.15,
yPosition: 15,
width: 3.07,
height: 0.5,
text: "Respaldado por:",
@ -131,7 +131,7 @@ const tHours = createSimpleTextP({
// Chile
const tChile = createSimpleTextP({
xPosition: 3.07,
yPosition: 15.98,
yPosition: 17.83,
width: 1.43,
height: 0.5,
text: "CHILE",
@ -144,7 +144,7 @@ const tChile = createSimpleTextP({
// Se expide certificado...
const tFinishLabel = createSimpleTextP({
xPosition: 8.22,
yPosition: 11.87,
yPosition: 12.68,
width: 13.15,
height: 0.5,
text: "Se expide el presente certificado para los fines que se estime conveniente",
@ -160,7 +160,7 @@ const photoSection = new Paragraph({
frame: {
position: {
x: cmText(25),
y: cmText(4.37),
y: cmText(5.2),
},
height: cmText(3.57),
width: cmText(2.81),
@ -199,13 +199,13 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
height: 2.5,
width: 2.5,
horizontalOffset: 25.85,
verticalOffset: 15.88,
verticalOffset: 17.49,
});
// FERNANDO ARAOZ
const tName = createSimpleTextP({
xPosition: 3.78,
yPosition: 5.21,
yPosition: 6.02,
width: 22.07,
height: 1.5,
text: props.personFullName,
@ -219,7 +219,7 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
frame: {
position: {
x: cmText(11.84),
y: cmText(6.54),
y: cmText(7.35),
},
width: cmText(6.02),
height: cmText(0.6),
@ -249,7 +249,7 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
frame: {
position: {
x: cmText(19.62),
y: cmText(17.58),
y: cmText(19.2),
},
width: cmText(5.87),
height: cmText(0.75),
@ -277,7 +277,7 @@ export async function supervisorEscolta(props: CertData<null>): Promise<Buffer>
// N° XXXX-20XX-EEG
const tCertCode = createSimpleTextP({
xPosition: 24.59,
yPosition: 8.07,
yPosition: 9,
width: 3.67,
height: 0.5,
text: `${props.certCode}-${props.certYear}-EEG`,

View File

@ -61,7 +61,9 @@ export async function getQR(data : {
verticalOffset: number,
behindDocument?: boolean,
}): Promise<ImageRun> {
const qr = await QR.toDataURL(`https://www.eegsac.com/alumnoscertificados.php?DNI=${data.dni}&iid=${data.iid}`, {margin: 1});
// Old URL: https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}
// New URL: https://eegsac.com/certificado/${dni()}
const qr = await QR.toDataURL(`https://eegsac.com/certificado/${data.dni}?iid=${data.iid}`, {margin: 1});
return new ImageRun({
data: qr,

View File

@ -3,7 +3,7 @@ import { renderToString } from "solid-js/web";
import { CertsBatch } from "src/views/BatchCerts";
import { template } from "./BatchCerts.template";
@Controller("batch-certs")
@Controller("batch-mode")
export class BatchCertController {
@Get()
entry(): string {

View File

@ -9,6 +9,7 @@ export function template(ssr: string): string {
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/static/styles.css?t=${Date.now()}" />
<link rel="icon" type="image/png" href="/static/favicon.png">
<!-- Phosphor icons -->
<link
rel="stylesheet"

View File

@ -184,7 +184,7 @@ export class CertificateService {
certificate.curso = data.subjectId;
certificate.codigo = await this.getNextRegisterCode(data.subjectId);
certificate.fecha_actual = new Date();
certificate.fecha_inscripcion = new Date(data.date);
certificate.fecha_inscripcion = data.date as unknown as Date ; // new Date(data.date);
certificate.curso_nombre = subject.nombre;
certificate.persona = person;

View File

@ -8,6 +8,7 @@ export function template(ssr: string): string {
<title>Registrar certificados - EEGSAC</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="stylesheet" href="/static/styles.css?t=${Date.now()}" />
<!-- Phosphor icons -->
<link

View File

@ -1,6 +1,7 @@
import { createSignal, Match, onMount, Show, Switch } from "solid-js";
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js";
import { Person } from "src/types/Person";
import { DniRegister } from "./DniEntry/DniRegister";
import { RegisterReturn } from "src/types/RegisterReturn";
/**
* Sample data
@ -27,9 +28,11 @@ enum Status {
Error,
}
export function DniEntry(props: {dni: string}) {
export function DniEntry(props: {dni: string, remove: (_: string) => void}) {
const [person, setPerson] = createSignal<Person | null>(null);
const [status, setStatus] = createSignal<Status>(Status.Empty);
const [certificates, setCertificates] = createSignal<Array<RegisterReturn>>([]);
const [certStatus, setCertStatus] = createSignal<Status>(Status.Empty);
const loadPerson = async() => {
setStatus(Status.Loading);
@ -40,7 +43,7 @@ export function DniEntry(props: {dni: string}) {
if (response.ok) {
setPerson(body);
setStatus(Status.Ok);
loadCertificates();
} else if (response.status === 404) {
console.error(body);
setStatus(Status.Error);
@ -58,6 +61,23 @@ export function DniEntry(props: {dni: string}) {
onMount(loadPerson);
const loadCertificates = async() => {
setCertStatus(Status.Loading);
try {
const response = await fetch(`/certificate/${props.dni}`);
if (response.ok) {
const body = await response.json();
setCertificates(body);
setCertStatus(Status.Ok);
} else {
setCertStatus(Status.Error);
}
} catch (e) {
setCertStatus(Status.Error);
}
};
return (
<>
<Show when={status() !== Status.Error}>
@ -96,11 +116,24 @@ export function DniEntry(props: {dni: string}) {
<div class="text-center">
<button
class={"rounded-full px-2 hover:bg-c-error first:text-c-error first:hover:text-c-on-error"}
onclick={() => props.remove(props.dni)}
>
<i class="ph ph-x"></i>
</button>
</div>
<div class="border-l"></div>
<div class="border-l">
<Switch>
<Match when={certStatus() === Status.Loading}>
<p>Loading...</p>
</Match>
<Match when={certStatus() === Status.Ok}>
<CertArray array={certificates()} />
</Match>
<Match when={certStatus() === Status.Error}>
<p class="text-c-error">Error loading certificates.</p>
</Match>
</Switch>
</div>
</Match>
</Switch>
@ -113,3 +146,34 @@ export function DniEntry(props: {dni: string}) {
</>
);
}
function CertArray(props: {array: Array<RegisterReturn>}) {
const filtered = createMemo(() => {
const nowMs = Date.now();
return props.array.filter((cert) => {
const date = new Date(cert.fecha_inscripcion);
const ms = date.getTime();
const difference = nowMs - ms;
return difference < (1000 * 60 * 60 * 24 * 366);
});
});
return (
<div>
[
<For each={filtered()}>
{(cert) => (
<div class="mr-4 inline-block underline">
{cert.curso_nombre}&nbsp;
<span class="font-mono">
{cert.fecha_inscripcion}
</span>
</div>
)}
</For>
]
</div>
);
}

View File

@ -1,10 +1,18 @@
import { For } from "solid-js";
import { For, createSignal, onMount } from "solid-js";
import { DniEntry } from "./DniEntry";
import { NewRegister } from "../components/NewRegister";
import { subjects } from "../subjects";
export function DniGroup(props: {group: string, index: number}) {
const dnis = () => [...props.group.matchAll(/\d+/g)];
const [dnis, setDnis] = createSignal<Array<string>>([]);
console.log("Loading group...");
onMount(() => {
setDnis([...props.group.matchAll(/\d+/g)].map((x) => x.toString()));
});
const removeDni = (dni: string) => {
setDnis((prev) => prev.filter((x) => x !== dni));
};
return (
<div class=" grid-cols-[53rem_auto] gap-2 my-8">
@ -19,7 +27,7 @@ export function DniGroup(props: {group: string, index: number}) {
</div>
<For each={dnis()}>
{(dni) => <DniEntry dni={dni.toString()} />}
{(dni) => <DniEntry dni={dni} remove={removeDni} />}
</For>
</div>
@ -27,7 +35,63 @@ export function DniGroup(props: {group: string, index: number}) {
<h2 class="font-medium text-xl text-c-success pb-2">
Grupo #{props.index + 1} - cursos y fechas
</h2>
<hr />
<CourseManager personAmount={dnis().length} />
</div>
</div>
);
}
function CourseManager(props: {personAmount: number}) {
const [courses, setCourses] = createSignal<Array<[number, string]>>([]);
async function registerCourse(_: number, subjectId: number, date: string): Promise<null | string> {
setCourses((x) => [...x, [subjectId, date]]);
return null;
}
return (
<div class="grid grid-cols-2 gap-2">
<NewRegister
personId={0}
onSuccess={() => {}}
registerFn={registerCourse}
labelText={"Seleccionar cursos"}
/>
<div class="p-4">
<h2 class="mb-4 font-bold text-xl">Verificar y confirmar cursos</h2>
<div class="">
<For each={courses()}>
{(course) => <Course
id={course[0]}
date={course[1]}
onDelete={(id) => setCourses((x) => x.filter((y) => y[0] !== id))}
/>}
</For>
</div>
<button
class="my-2 bg-c-primary text-c-on-primary px-4 py-2 rounded-md cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed"
type="button"
>
Registrar las <b>{props.personAmount} personas</b> en los <b>{courses().length} cursos</b>
</button>
</div>
</div>
);
}
function Course(props: {id: number, date: string, onDelete: (id: number) => void}) {
const courseName = () => subjects().find((x) => x.id === props.id)?.nombre ?? "NOT FOUND";
return (
<div class="grid grid-cols-[auto_10rem_5rem] gap-2 hover:bg-c-surface-variant hover:text-c-on-surface-variant font-mono">
<p>{courseName()}</p>
<p>{props.date}</p>
<div>
<button class="underline" onclick={() => props.onDelete(props.id)}>Eliminar</button>
</div>
</div>
);

View File

@ -1,10 +1,13 @@
import { createSignal } from "solid-js";
import { createSignal, onMount } from "solid-js";
import { DniTable } from "./AulaVirtual/DniTable";
import { Dnis } from "./AulaVirtual/Dnis";
import { ensureColors } from "./components/colors";
export function CertsBatch() {
const [dniGroups, setDniGroups] = createSignal<Array<string>>([]);
onMount(ensureColors);
return (
<div>
<h1 class="px-4 py-2 text-2xl font-bold">

View File

@ -1,27 +1,24 @@
import { createSignal } from "solid-js";
import { createSignal, onMount } from "solid-js";
import { Search } from "./components/Search";
import { Person } from "../types/Person";
import { Registers } from "./components/Registers";
import { NewRegister } from "./components/NewRegister";
import { ensureColors } from "./components/colors";
export function Certs() {
const [person, setPerson] = createSignal<Person | null>(null);
const [lastUpdate, setLastUpdate] = createSignal(0);
return (
<div>
<h1 class="px-4 py-2 text-2xl font-bold">
Registrar certificado
</h1>
<Search setPerson={setPerson}/>
<div class="grid grid-cols-2 gap-2">
<Registers person={person()} lastUpdate={lastUpdate()} />
<NewRegister
person={person()}
onSuccess={() => setLastUpdate((x) => x + 1)}
/>
</div>
onMount(ensureColors);
return (
<div class="grid grid-cols-[18rem_25rem_1fr]">
<Search setPerson={setPerson}/>
<NewRegister
personId={person()?.id ?? null}
onSuccess={() => setLastUpdate((x) => x + 1)}
/>
<Registers person={person()} lastUpdate={lastUpdate()} />
</div>
);
}

View File

@ -0,0 +1,9 @@
import type { JSX } from "solid-js";
export function FilledCard(props: {children?: Array<JSX.Element> | JSX.Element, class?: string}) {
return (
<div class={`bg-c-surface-variant text-c-on-surface-variant rounded-xl m-2 shadow ${props.class ?? ""}`}>
{props.children}
</div>
);
}

View File

@ -1,16 +1,83 @@
import { createSignal, onMount, Show } from "solid-js";
import type { CursoGIE } from "../../model/CursoGIE/cursoGIE.entity";
import { createSignal, Show } from "solid-js";
import { SearchableSelect } from "./NewRegister/SearchableSelect";
import { JSX } from "solid-js/jsx-runtime";
import { Person } from "src/types/Person";
import { subjects } from "../subjects";
import { FilledCard } from "./FilledCard";
import { RegisterPreview } from "./NewRegister/RegisterPreview";
type HTMLEventFn = JSX.EventHandlerUnion<HTMLFormElement, Event & {
submitter: HTMLElement;
}>;
export function NewRegister(props: {person: Person | null, onSuccess: () => void}) {
const [subjects, setSubjects] = createSignal<Array<CursoGIE>>([]);
async function defaultNewRegisterFn(personId: number, subjectId: number, date: string): Promise<null | string> {
const response = await fetch("/certificate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
personId,
subjectId,
date,
}),
});
if (response.ok) {
return null;
} else {
const data = await response.json();
return JSON.stringify(data);
}
}
type RegisterFn = (personId: number, subjectId: number, date: string) => Promise<null | string>;
const defaultLabelText = "3. Crear nuevos registros";
type TabType = "Presets" | "Manual";
export function NewRegister(props: {
personId: number | null,
onSuccess: () => void,
registerFn?: RegisterFn,
labelText?: string,
}) {
const [active, setActive] = createSignal<TabType>("Manual");
const [selections, setSelections] = createSignal<Array<[number, string]>>([]);
const onRegister = () => {
setSelections([]);
props.onSuccess();
};
return (
<div class="h-screen overflow-y-scroll">
<FilledCard class="border border-c-outline overflow-hidden">
<h2 class="p-4 font-bold text-xl">{props.labelText ?? defaultLabelText}</h2>
<RegisterTabs active={active()} setActive={setActive} />
<div class="bg-c-surface p-4 h-[22rem]">
<Show when={active() === "Presets"}>
<p>Proximamente...</p>
</Show>
<Show when={active() === "Manual"}>
<ManualCerts personId={props.personId} onAdd={(v) => setSelections((x) => [...x, v])} />
</Show>
</div>
</FilledCard>
<RegisterPreview
selections={selections()}
personId={props.personId}
onDelete={(deleteId) => setSelections((s) => [...s.filter(([id]) => id !== deleteId)])}
onRegister={onRegister}
/>
</div>
);
const [error, setError] = createSignal("");
const [loading, setLoading] = createSignal(false);
// Used to update SearchableSelect.tsx manually
@ -27,23 +94,6 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
/>
);
// Loads subjects from DB
onMount(async() => {
console.log("Retrieve all subjects");
try {
const response = await fetch("/subject/");
if (response.ok) {
setSubjects(await response.json());
} else {
setError("No se pudo cargar cursos");
}
} catch (e) {
setError(`No se pudo cargar cursos. ${JSON.stringify(e)}`);
}
});
const register: HTMLEventFn = async(ev) => {
ev.preventDefault();
@ -65,24 +115,17 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
setLoading(true);
const response = await fetch("/certificate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
personId: props.person?.id ?? -1,
subjectId: subject,
date,
}),
});
const result = await (props.registerFn ?? defaultNewRegisterFn)(
props.personId ?? -1,
subject,
date,
);
if (response.ok) {
if (result === null) {
props.onSuccess();
setCount((x) => x + 1);
} else {
const data = await response.json();
setError(JSON.stringify(data));
setError(result);
}
setLoading(false);
@ -90,9 +133,9 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
return (
<div class="p-4">
<h2 class="mb-4 font-bold text-xl">3. Crear nuevos registros</h2>
<h2 class="mb-4 font-bold text-xl">{props.labelText ?? defaultLabelText}</h2>
<Show when={props.person?.id !== -1}>
<Show when={props.personId !== -1}>
<form
class="px-4 grid"
style={{"grid-template-columns": "30rem 12rem 10rem auto", "grid-column-gap": "1rem"}}
@ -133,3 +176,104 @@ export function NewRegister(props: {person: Person | null, onSuccess: () => void
</div>
);
}
function RegisterTabs(props: {active: TabType, setActive: (v: TabType) => void}) {
const presetsClasses = () => ((props.active === "Presets") ? "font-bold border-c-primary" : "border-c-transparent");
const manualClasses = () => ((props.active === "Manual") ? "font-bold border-c-primary" : "border-c-transparent");
return (
<div class="grid grid-cols-2">
<button
class={`py-2 border-b-4 ${presetsClasses()}`}
onclick={() => props.setActive("Presets")}
>
Presets
</button>
<button
class={`py-2 border-b-4 ${manualClasses()}`}
onclick={() => props.setActive("Manual")}
>
Manual
</button>
</div>
);
}
function ManualCerts(props: {personId: number | null, onAdd: (v: [number, string]) => void}) {
// Used to update SearchableSelect.tsx manually
const [count, setCount] = createSignal(0);
const [error, setError] = createSignal("");
const [selectedSubject, setSelectedSubject] = createSignal<number | null>(null);
const datePicker = (
<input
id="create-date"
class="bg-c-surface text-c-on-surface border border-c-outline rounded-lg p-2"
type="date"
/>
);
const register: HTMLEventFn = async(ev) => {
ev.preventDefault();
const subject = selectedSubject();
const date = (datePicker as HTMLInputElement).value;
if (subject === null) {
setError("Selecciona un curso");
setTimeout(() => setError(""), 5000);
return;
}
if (date === "") {
setError("Selecciona una fecha");
setTimeout(() => setError(""), 5000);
return;
}
props.onAdd([subject, date]);
// This is used to update & refresh the <SearchableSelect> component
setCount((x) => x + 1);
};
console.log(`Person ID: ${props.personId}`);
return (
<>
<form onsubmit={register}>
<div>
<SearchableSelect
subjects={subjects()}
onChange={setSelectedSubject}
count={count()}
/>
</div>
<div class="relative my-4">
{datePicker}
<label for="create-date" class="absolute -top-2 left-2 text-xs bg-c-surface px-1">Fecha</label>
</div>
<input
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed"
type="submit"
value="Agregar"
disabled={props.personId === null}
/>
</form>
<p
class="my-2 p-1 rounded w-fit mx-4 bg-c-error text-c-on-error"
style={{opacity: error() === "" ? "0" : "1", "user-select": "none"}}
>
{error()}&nbsp;
</p>
</>
);
}

View File

@ -0,0 +1,95 @@
import { FilledCard } from "../FilledCard";
import { For } from "solid-js";
import { XIcon } from "src/views/icons/XIcon";
import { subjects } from "src/views/subjects";
function isoDateToLocalDate(date: string): string {
const [,month, day] = /\d{4}-(\d{2})-(\d{2})/.exec(date) ?? "";
return `${day}/${month}`;
}
export function RegisterPreview(props: {selections: Array<[number, string]>, personId: number | null, onDelete: (v: number) => void, onRegister: () => void}) {
const submit = async() => {
console.log("Submit...");
for (const [courseId, date] of props.selections) {
const result = await defaultNewRegisterFn(
props.personId ?? -1,
courseId,
date,
);
if (result === null) {
console.log("Success");
} else {
console.log(`error. ${result}`);
}
}
props.onRegister();
};
return (
<FilledCard class="border border-c-outline overflow-hidden">
<h2 class="p-4 font-bold text-xl">Confirmar registro</h2>
<div class="bg-c-surface p-4">
<For each={props.selections}>
{([courseId, date]) => <Register courseId={courseId} date={date} onDelete={props.onDelete} />}
</For>
<button
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-full cursor-pointer mt-4
disabled:opacity-50 disabled:cursor-not-allowed"
type="button"
disabled={props.selections.length === 0}
onclick={submit}
>
Registrar los {props.selections.length} cursos
</button>
</div>
</FilledCard>
);
}
function Register(props: {courseId: number, date: string, onDelete: (v: number) => void}) {
const courseName = () => {
const courses = subjects();
return courses.find((c) => c.id === props.courseId)?.nombre ?? "!";
};
return (
<div class="grid grid-cols-[auto_4rem_1.5rem] py-1 px-2 rounded-md border border-c-outline my-1">
{courseName()}
<span class="font-mono">{isoDateToLocalDate(props.date)}</span>
<button
class="hover:bg-c-surface-variant rounded-md"
onclick={() => props.onDelete(props.courseId)}
>
<XIcon fill="var(--c-on-surface)" />
</button>
</div>
);
}
async function defaultNewRegisterFn(personId: number, subjectId: number, date: string): Promise<null | string> {
const response = await fetch("/certificate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
personId,
subjectId,
date,
}),
});
if (response.ok) {
return null;
} else {
const data = await response.json();
return JSON.stringify(data);
}
}

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, For, JSX, Show } from "solid-js";
import { createEffect, createSignal, For } from "solid-js";
import type {CursoGIE} from "../../../model/CursoGIE/cursoGIE.entity";
import { isServer } from "solid-js/web";
@ -15,7 +15,14 @@ export function SearchableSelect(props: {
const inputEl = ev.target as HTMLInputElement;
// Clear current selection
setSelected(null);
setFilter(inputEl.value.toLowerCase());
let filter: string = inputEl.value.toLowerCase();
filter = filter.replace("á", "a");
filter = filter.replace("é", "e");
filter = filter.replace("í", "i");
filter = filter.replace("ó", "o");
filter = filter.replace("ú", "u");
setFilter(filter);
};
createEffect(() => {
@ -58,23 +65,36 @@ export function SearchableSelect(props: {
});
}
const filteredOptions = () => {
const filterText = filter();
return props.subjects.filter((subject) => {
let subjectText = subject.nombre.toLowerCase();
subjectText = subjectText.replace("á", "a");
subjectText = subjectText.replace("é", "e");
subjectText = subjectText.replace("í", "i");
subjectText = subjectText.replace("ó", "o");
subjectText = subjectText.replace("ú", "u");
return selected() === null && subjectText.indexOf(filterText) !== -1;
});
};
return (
<>
{inputElement}
<br/>
<div
class="border-c-outline border-2 rounded overflow-y-scroll"
style={{"max-height": "18rem", "min-height": "12rem"}}
class="border-c-outline border-2 rounded overflow-y-scroll h-[10rem]"
>
<For each={props.subjects}>
<For each={filteredOptions()}>
{(s) => (
<button
class={`w-full text-left py-1 px-2
hover:bg-c-primary-container hover:text-c-on-primary-container
${s.nombre.toLowerCase().indexOf(filter()) !== -1 && selected() === null ? "block" : "hidden"}`}
class="w-full text-left py-1 px-2
hover:bg-c-primary-container hover:text-c-on-primary-container"
onclick={(ev) => {
ev.preventDefault();
console.log("Click! :D");
setSelected(s.id);
setInputValue(s.nombre);
}}

View File

@ -1,4 +1,4 @@
import { Show, createEffect, createSignal, For, JSX } from "solid-js";
import { Show, createEffect, createSignal, For, JSX, createMemo } from "solid-js";
import { Person } from "../../types/Person";
import { RegisterReturn } from "../../types/RegisterReturn";
@ -33,57 +33,12 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
setLoading(false);
};
const sortedRegisters = createMemo(() => registers().sort((r1, r2) => ((r1.fecha_inscripcion < r2.fecha_inscripcion) ? 1 : -1)));
return (
<div class="p-4">
<h2 class="mb-4 font-bold text-xl">2. Revisar registros actuales</h2>
<div class="px-4">
<Show when={props.person !== null}>
<div>
<input
type="text"
disabled
value={props.person?.nombreCompleto}
class="bg-c-background text-c-on-background border-c-outline border rounded px-2 py-1
disabled:cursor-text w-full"
/>
</div>
<div class="grid grid-cols-[3fr_3fr_2fr_2fr_1fr_1fr] gap-1">
<CopyButton
copyText={`${props.person!.nombres} ${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
>
NOM <b>AP</b>
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno} ${props.person!.nombres}`}
>
<b>AP</b> NOM
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
</CopyButton>
<CopyButton
copyText={`${props.person!.nombres}`}
>
NOM
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
>
<b>AP</b>
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoPaterno}`}
>
<b><i>Ap</i></b>
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoMaterno}`}
>
<b><i>Am</i></b>
</CopyButton>
</div>
<p
class="my-2"
style={{ display: loading() ? "block" : "none" }}
@ -105,7 +60,7 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
</thead>
<tbody>
<For each={registers().sort((r1, r2) => ((r1.fecha_actual < r2.fecha_actual) ? 1 : -1))}>
<For each={sortedRegisters()}>
{(register) => <Register cert={register} onUpdate={loadCertificates} />}
</For>
</tbody>

View File

@ -3,6 +3,9 @@ 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;
@ -19,11 +22,14 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
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() !== "") {
QR.toDataURL(`https://www.eegsac.com/alumnoscertificados.php?DNI=${dni()}`, {margin: 1}, (err, res) => {
// 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;
@ -51,6 +57,7 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
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);
@ -67,30 +74,55 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
setLoading(false);
};
return (
<div class="p-4 grid" style={{"grid-template-columns": "20rem auto"}}>
<div>
<h2 class="my-2 font-bold text-xl">1. Buscar persona</h2>
<form onSubmit={searchDNI} class="px-4">
<label for="search-dni">DNI</label>
<br />
<InputBox dni={dni()} setDni={setDni} loading={loading()} />
const nombresYApellidos = () => {
const p = persona();
if (p === null) {
return "";
}
return `${p.nombres} ${p.apellidoPaterno} ${p.apellidoMaterno}`;
};
<br />
<input
class="bg-c-primary text-c-on-primary px-4 py-2 rounded-md cursor-pointer
disabled:opacity-50 disabled:cursor-not-allowed"
type="submit"
value="Buscar"
disabled={loading()}
/>
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="my-2 p-1 rounded w-fit mx-4 bg-c-error text-c-on-error"
style={{display: error() === "" ? "none" : "block"}}
>
Error:
Error:
<br />
{error()}
</p>
@ -104,13 +136,45 @@ export function Search(props: {setPerson: (p: Person | null) => void}) {
{warning()}
</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)" />
&nbsp;
Nombres y <b>Apellidos</b>
</CopyButton>
<CopyButton copyText={apellidosYNombres()}>
<CopyIcon fill="var(--c-on-primary)" />
&nbsp;
<b>Apellidos</b> y Nombres
</CopyButton>
<div class="grid grid-cols-2 gap-2">
<CopyButton copyText={apellidos()}>
<CopyIcon fill="var(--c-on-primary)" />
&nbsp;
<b>Apellidos</b>
</CopyButton>
<CopyButton copyText={nombres()}>
<CopyIcon fill="var(--c-on-primary)" />
&nbsp;
Nombres
</CopyButton>
</div>
</div>
</div>
<Show when={warning() !== ""}>
<RegisterPerson dni={dni()} onSuccess={search} />
</Show>
</div>
<div>
<img src={qrBase64() ?? ""} height="150" width="150" />
</div>
</div>
);
}
@ -120,12 +184,10 @@ function InputBox(props: {
dni: string,
setDni: (v: string) => void,
}) {
const [successAnimation, setSuccessAnimation] = createSignal(false);
const inputElement = (
<input
id="search-dni"
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1
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"
@ -146,8 +208,6 @@ function InputBox(props: {
if (props.dni.length === 8) {
navigator.clipboard.writeText(props.dni);
setSuccessAnimation(true);
setTimeout(() => setSuccessAnimation(false), 1000);
}
};
@ -159,18 +219,86 @@ function InputBox(props: {
};
return (
<div class="grid gap-2 grid-cols-[10rem_3rem_3rem]">
<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={`${successAnimation() ? "bg-c-success" : "bg-c-primary"} rounded transition-colors`}
onclick={copyToClipboard}
type="button"
class="absolute top-1 right-[3.75rem] rounded hover:bg-c-surface-variant"
>
<i class={`${successAnimation() ? "text-c-on-success" : "text-c-on-primary"} ph ph-clipboard-text text-2xl align-middle`}></i>
<MagnifyingGlassIcon fill="var(--c-on-surface)" />
</button>
<button class="bg-c-error rounded" onclick={clearDni} type="button">
<i class="ph ph-trash text-2xl text-c-on-primary align-middle"></i>
<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">&nbsp;</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>
);
}

View File

@ -0,0 +1,166 @@
import { isServer } from "solid-js/web";
const accentOrange = "";
const accentGreen = `
:root {
--c-primary: #7cdc6d;
--c-on-primary: #003a02;
--c-primary-container: #005304;
--c-on-primary-container: #97f986;
--c-background: #1a1c18;
--c-on-background: #e2e3dd;
--c-surface: #1a1c18;
--c-on-surface: #e2e3dd;
--c-outline: #8d9387;
--c-surface-variant: #43483f;
--c-on-surface-variant: #c3c8bc;
}
@media (prefers-color-scheme: light) {
:root {
--c-primary: #006e08;
--c-on-primary: #ffffff;
--c-primary-container: #97f986;
--c-on-primary-container: #002201;
--c-background: #fcfdf6;
--c-on-background: #1a1c18;
--c-surface: #fcfdf6;
--c-on-surface: #1a1c18;
--c-outline: #73796e;
--c-surface-variant: #dfe4d8;
--c-on-surface-variant: #43483f;
}
}
`;
const accentBlue = `
:root {
--c-primary: #adc6ff;
--c-on-primary: #002e69;
--c-primary-container: #0e448e;
--c-on-primary-container: #d8e2ff;
--c-background: #1b1b1f;
--c-on-background: #e3e2e6;
--c-surface: #1b1b1f;
--c-on-surface: #e3e2e6;
--c-outline: #8e9099;
--c-surface-variant: #44474f;
--c-on-surface-variant: #c4c6d0;
}
@media (prefers-color-scheme: light) {
:root {
--c-primary: #315da8;
--c-on-primary: #ffffff;
--c-primary-container: #d8e2ff;
--c-on-primary-container: #001a41;
--c-background: #fefbff;
--c-on-background: #1b1b1f;
--c-surface: #fefbff;
--c-on-surface: #1b1b1f;
--c-outline: #74777f;
--c-surface-variant: #e1e2ec;
--c-on-surface-variant: #44474f;
}
}
`;
const accentYellow = `
:root {
--c-primary: #f5bf31;
--c-on-primary: #3f2e00;
--c-primary-container: #5b4300;
--c-on-primary-container: #ffdf9b;
--c-background: #1e1b16;
--c-on-background: #e9e1d9;
--c-surface: #1e1b16;
--c-on-surface: #e9e1d9;
--c-outline: #999080;
--c-surface-variant: #4d4639;
--c-on-surface-variant: #d0c5b4;
}
@media (prefers-color-scheme: light) {
:root {
--c-primary: #785a00;
--c-on-primary: #ffffff;
--c-primary-container: #ffdf9b;
--c-on-primary-container: #251a00;
--c-background: #fffbff;
--c-on-background: #1e1b16;
--c-surface: #fffbff;
--c-on-surface: #1e1b16;
--c-outline: #7f7667;
--c-surface-variant: #ede1cf;
--c-on-surface-variant: #4d4639;
}
}
`;
const accentPink = `
:root {
--c-primary: #ffade5;
--c-on-primary: #5e0051;
--c-primary-container: #80156e;
--c-on-primary-container: #ffd7ef;
--c-background: #1f1a1d;
--c-on-background: #eae0e3;
--c-surface: #1f1a1d;
--c-on-surface: #eae0e3;
--c-outline: #9b8d94;
--c-surface-variant: #4f444a;
--c-on-surface-variant: #d2c2ca;
}
@media (prefers-color-scheme: light) {
:root {
--c-primary: #9d3288;
--c-on-primary: #ffffff;
--c-primary-container: #ffd7ef;
--c-on-primary-container: #3a0031;
--c-background: #fffbff;
--c-on-background: #1f1a1d;
--c-surface: #fffbff;
--c-on-surface: #1f1a1d;
--c-outline: #81737a;
--c-surface-variant: #efdee6;
--c-on-surface-variant: #4f444a;
}
}
`;
export const ensureColors = () => {
if (!isServer) {
console.log("Running in the client!");
const colorScheme = localStorage.getItem("color-scheme") ?? "orange";
const styleEl = document.createElement("style");
switch (colorScheme) {
case "orange": {
styleEl.innerHTML = accentOrange;
break;
}
case "green": {
styleEl.innerHTML = accentGreen;
break;
}
case "blue": {
styleEl.innerHTML = accentBlue;
break;
}
case "yellow": {
styleEl.innerHTML = accentYellow;
break;
}
case "pink": {
styleEl.innerHTML = accentPink;
break;
}
}
document.head.appendChild(styleEl);
}
};

View File

@ -0,0 +1,5 @@
export function CopyIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function DocxIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M52,144H36a8,8,0,0,0-8,8v56a8,8,0,0,0,8,8H52a36,36,0,0,0,0-72Zm0,56H44V160h8a20,20,0,0,1,0,40Zm169.53-4.91a8,8,0,0,1,.25,11.31A30.06,30.06,0,0,1,200,216c-17.65,0-32-16.15-32-36s14.35-36,32-36a30.06,30.06,0,0,1,21.78,9.6,8,8,0,0,1-11.56,11.06A14.24,14.24,0,0,0,200,160c-8.82,0-16,9-16,20s7.18,20,16,20a14.24,14.24,0,0,0,10.22-4.66A8,8,0,0,1,221.53,195.09ZM128,144c-17.65,0-32,16.15-32,36s14.35,36,32,36,32-16.15,32-36S145.65,144,128,144Zm0,56c-8.82,0-16-9-16-20s7.18-20,16-20,16,9,16,20S136.82,200,128,200ZM48,120a8,8,0,0,0,8-8V40h88V88a8,8,0,0,0,8,8h48v16a8,8,0,0,0,16,0V88a8,8,0,0,0-2.34-5.66l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72A8,8,0,0,0,48,120ZM160,51.31,188.69,80H160Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function DownloadIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M240,136v64a16,16,0,0,1-16,16H32a16,16,0,0,1-16-16V136a16,16,0,0,1,16-16H72a8,8,0,0,1,0,16H32v64H224V136H184a8,8,0,0,1,0-16h40A16,16,0,0,1,240,136Zm-117.66-2.34a8,8,0,0,0,11.32,0l48-48a8,8,0,0,0-11.32-11.32L136,108.69V24a8,8,0,0,0-16,0v84.69L85.66,74.34A8,8,0,0,0,74.34,85.66ZM200,168a12,12,0,1,0-12,12A12,12,0,0,0,200,168Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function HomeIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M218.83,103.77l-80-75.48a1.14,1.14,0,0,1-.11-.11,16,16,0,0,0-21.53,0l-.11.11L37.17,103.77A16,16,0,0,0,32,115.55V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V115.55A16,16,0,0,0,218.83,103.77ZM208,208H48V115.55l.11-.1L128,40l79.9,75.43.11.1Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function KeyIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M48,56V200a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0Zm84,54.5L112,117V96a8,8,0,0,0-16,0v21L76,110.5a8,8,0,0,0-5,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,132,110.5ZM238,115.64A8,8,0,0,0,228,110.5L208,117V96a8,8,0,0,0-16,0v21l-20-6.49a8,8,0,0,0-4.95,15.22l20,6.49-12.34,17a8,8,0,1,0,12.94,9.4l12.34-17,12.34,17a8,8,0,1,0,12.94-9.4l-12.34-17,20-6.49A8,8,0,0,0,238,115.64Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function MagnifyingGlassIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function PaletteIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256" style="--darkreader-inline-fill: #000000;" data-darkreader-inline-fill=""><path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function ScanIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M224,40V80a8,8,0,0,1-16,0V48H176a8,8,0,0,1,0-16h40A8,8,0,0,1,224,40ZM80,208H48V176a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H80a8,8,0,0,0,0-16Zm136-40a8,8,0,0,0-8,8v32H176a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V176A8,8,0,0,0,216,168ZM40,88a8,8,0,0,0,8-8V48H80a8,8,0,0,0,0-16H40a8,8,0,0,0-8,8V80A8,8,0,0,0,40,88Zm128,96H88a16,16,0,0,1-16-16V88A16,16,0,0,1,88,72h80a16,16,0,0,1,16,16v80A16,16,0,0,1,168,184ZM88,168h80V88H88Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function StackIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill="var(--c-on-surface-variant)" viewBox="0 0 256 256"><path d="M230.91,172A8,8,0,0,1,228,182.91l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,36,169.09l92,53.65,92-53.65A8,8,0,0,1,230.91,172ZM220,121.09l-92,53.65L36,121.09A8,8,0,0,0,28,134.91l96,56a8,8,0,0,0,8.06,0l96-56A8,8,0,1,0,220,121.09ZM24,80a8,8,0,0,1,4-6.91l96-56a8,8,0,0,1,8.06,0l96,56a8,8,0,0,1,0,13.82l-96,56a8,8,0,0,1-8.06,0l-96-56A8,8,0,0,1,24,80Zm23.88,0L128,126.74,208.12,80,128,33.26Z"></path></svg>
);
}

View File

@ -0,0 +1,5 @@
export function TrashIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
);
}

View File

@ -0,0 +1,6 @@
export function XIcon(props: {fill: string}) {
return (
<svg xmlns="http://www.w3.org/2000/svg" class="inline-block w-6" fill={props.fill} viewBox="0 0 256 256"><path d="M165.66,101.66,139.31,128l26.35,26.34a8,8,0,0,1-11.32,11.32L128,139.31l-26.34,26.35a8,8,0,0,1-11.32-11.32L116.69,128,90.34,101.66a8,8,0,0,1,11.32-11.32L128,116.69l26.34-26.35a8,8,0,0,1,11.32,11.32ZM232,128A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>
);
}

16
src/views/subjects.ts Normal file
View File

@ -0,0 +1,16 @@
import { createSignal } from "solid-js";
import { CursoGIE } from "src/model/CursoGIE/cursoGIE.entity";
export const [subjects, setSubjects] = createSignal<Array<CursoGIE>>([]);
(async() => {
try {
const response = await fetch("/subject/");
if (response.ok) {
setSubjects(await response.json());
}
} catch (e) {
/* empty */
}
})();

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB