[FE] Begin interface for Aulavirtual

master
Araozu 2023-06-11 22:25:18 -05:00
parent 87bf255635
commit 5a8b93edc2
13 changed files with 223 additions and 89 deletions

4
.gitignore vendored
View File

@ -44,5 +44,5 @@ lerna-debug.log*
# Tailwind output # Tailwind output
static/styles.css static/styles.css
# Auto generated Solid hydration script # Auto generated Solid hydration scripts
static/hydration.js static/*.js

View File

@ -46,3 +46,24 @@ build({
outdir: "static", outdir: "static",
format: "cjs", format: "cjs",
}); });
build({
platform: "browser",
entryPoints: [
"src/views/hydration/hydration_aulavirtual.ts",
],
bundle: true,
minify: true,
sourcemap: false,
logLevel: "info",
plugins: [
solidPlugin({
solid: {
generate: "dom",
hydratable: true,
},
})
],
outdir: "static",
format: "cjs",
});

View File

@ -58,3 +58,28 @@ const { glob } = require("glob");
await ctx.watch(); await ctx.watch();
console.log("Watching hydration script..."); console.log("Watching hydration script...");
})(); })();
(async () => {
const ctx = await context({
platform: "browser",
entryPoints: [
"src/views/hydration/hydration_aulavirtual.ts",
],
bundle: true,
minify: false,
logLevel: "info",
plugins: [
solidPlugin({
solid: {
generate: "dom",
hydratable: true,
},
})
],
outdir: "static",
format: "cjs",
});
await ctx.watch();
console.log("Watching hydration script...");
})();

View File

@ -1,6 +1,6 @@
/* @refresh reload */ /* @refresh reload */
import { render } from 'solid-js/web'; import { render } from 'solid-js/web';
import { Certs } from "../../src/views/Certs"; import { AulaVirtual } from "../../src/views/AulaVirtual";
import "../../static/tailwind.css" import "../../static/tailwind.css"
render(() => <Certs />, document.getElementById("root")!); render(() => <AulaVirtual />, document.getElementById("root")!);

View File

@ -10,6 +10,7 @@ import { PersonService } from "./controller/person/person.service";
import { SubjectController } from "./controller/subject/subject.controller"; import { SubjectController } from "./controller/subject/subject.controller";
import { SubjectService } from "./controller/subject/subject.service"; import { SubjectService } from "./controller/subject/subject.service";
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { AulaVirtualController } from "./controller/aulavirtual/aulavirtual.controller";
// Must be done before initializing DB. // Must be done before initializing DB.
console.log(dotenv.config()); console.log(dotenv.config());
@ -44,7 +45,7 @@ const db = process.env.MY_SQL_DB;
synchronize: false, synchronize: false,
}), }),
], ],
controllers: [CertificateController, PersonController, SubjectController], controllers: [CertificateController, PersonController, SubjectController, AulaVirtualController],
providers: [CertificateService, PersonService, SubjectService], providers: [CertificateService, PersonService, SubjectService],
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,12 +1,10 @@
import { import {
Document, Packer, Paragraph, PageOrientation, ImageRun, Document, Packer, Paragraph, PageOrientation,
HorizontalPositionRelativeFrom, VerticalPositionRelativeFrom,
FrameAnchorType, FrameAnchorType,
TextRun, TextRun,
AlignmentType, AlignmentType,
} from "docx"; } from "docx";
import * as QR from "qrcode"; import { Matpel, cmText, getImage, getMatpelHours, getMatpelLabel, getQR } from "./utils";
import { Matpel, cm, cmText, cmToEmu, getImage, getMatpelHours, getMatpelLabel } from "./utils";
import { CertData } from "./CertData"; import { CertData } from "./CertData";
@ -309,25 +307,13 @@ export async function matpelCert(props: CertData<Matpel>): Promise<Buffer> {
alignment: AlignmentType.LEFT, alignment: AlignmentType.LEFT,
}); });
const qr = await QR.toDataURL(`https://www.eegsac.com/alumnoscertificados.php?DNI=${props.personDni}`, {margin: 1}); const imgQR = await getQR({
iid: props.certIId,
const imgQR = new ImageRun({ dni: props.personDni,
data: qr, height: 2.47,
transformation: { width: 2.47,
height: cm(2.47), horizontalOffset: 26.3,
width: cm(2.47), verticalOffset: 16.48,
},
floating: {
zIndex: 1,
horizontalPosition: {
relative: HorizontalPositionRelativeFrom.LEFT_MARGIN,
offset: cmToEmu(26.3),
},
verticalPosition: {
relative: VerticalPositionRelativeFrom.TOP_MARGIN,
offset: cmToEmu(16.48),
},
},
}); });
const doc = new Document({ const doc = new Document({

View File

@ -0,0 +1,13 @@
import { Controller, Get } from "@nestjs/common";
import { renderToString } from "solid-js/web";
import { template } from "./aulavirtual.template";
import { AulaVirtual } from "src/views/AulaVirtual";
@Controller("aulavirtual")
export class AulaVirtualController {
@Get()
entry(): string {
const html = renderToString(AulaVirtual);
return template(html);
}
}

View File

@ -0,0 +1,28 @@
import { generateHydrationScript } from "solid-js/web";
export function template(ssr: string): string {
return `
<!DOCTYPE html>
<html lang="es">
<head>
<title>Aula Virtual - EEGSAC</title>
<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()}" />
<!-- Phosphor icons -->
<link
rel="stylesheet"
type="text/css"
href="https://unpkg.com/@phosphor-icons/web@2.0.3/src/regular/style.css"
/>
${generateHydrationScript()}
</head>
<body>
<div id="root">
${ssr}
</div>
</body>
<script src="/static/hydration_aulavirtual.js?t=${Date.now()}"></script>
</html>
`;
}

64
src/views/AulaVirtual.tsx Normal file
View File

@ -0,0 +1,64 @@
import { Hydration } from "solid-js/web";
export function AulaVirtual() {
return (
<div>
<h1 class="px-4 py-2 text-2xl font-bold">
Aula Virtual
</h1>
<h2 class="my-2 font-bold text-xl">1. Obtener sesión de Chamillo</h2>
<form>
<div>
<label for="aula-login">Usuario: </label>
<br />
<input
id="aula-login"
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1
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"
placeholder="Usuario"
/* value={props.dni} */
required={true}
/* onChange={(e) => props.setDni(e.target.value)} */
/* disabled={props.loading} */
/>
</div>
<div>
<label for="aula-password">Contraseña: </label>
<br />
<input
id="aula-password"
class="bg-c-background text-c-on-background border-c-outline border-2 rounded px-2 py-1
invalid:border-c-error invalid:text-c-error
focus:border-c-primary outline-none font-mono
disabled:opacity-50 disabled:cursor-not-allowed"
type="password"
placeholder="Contraseña"
/* value={props.dni} */
required={true}
/* onChange={(e) => props.setDni(e.target.value)} */
/* disabled={props.loading} */
/>
</div>
<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="Iniciar sesión"
/* disabled={loading()} */
/>
</form>
<h2 class="my-2 font-bold text-xl">2. Buscar Persona</h2>
<h2 class="my-2 font-bold text-xl">3. Inscribir en cursos</h2>
<h2 class="my-2 font-bold text-xl">4. Generar mensaje de bienvenida</h2>
</div>
);
}

View File

@ -16,10 +16,7 @@ export function Certs() {
Registrar certificado Registrar certificado
</h1> </h1>
<Search setPerson={setPerson}/> <Search setPerson={setPerson}/>
<div <div class="grid grid-cols-2 gap-2">
class="grid"
style={{"grid-template-columns": "50% 50%"}}
>
<Registers person={person()} lastUpdate={lastUpdate()} /> <Registers person={person()} lastUpdate={lastUpdate()} />
<NewRegister <NewRegister
person={person()} person={person()}

View File

@ -50,48 +50,39 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
/> />
</div> </div>
<div> <div class="grid grid-cols-[3fr_3fr_2fr_2fr_1fr_1fr] gap-1">
<div class="grid grid-cols-2 gap-1 font-mono"> <CopyButton
<CopyButton copyText={`${props.person!.nombres} ${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
copyText={`${props.person!.nombres} ${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`} >
> NOM <b>AP</b>
Nombres y <b>Apellidos</b> <i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i> </CopyButton>
</CopyButton> <CopyButton
<CopyButton copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno} ${props.person!.nombres}`}
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno} ${props.person!.nombres}`} >
> <b>AP</b> NOM
<b>Apellidos</b> y Nombres <i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i> </CopyButton>
</CopyButton> <CopyButton
</div> copyText={`${props.person!.nombres}`}
>
<div class="grid grid-cols-[2fr_2fr_1fr_1fr] gap-1 font-mono"> NOM
<CopyButton </CopyButton>
copyText={`${props.person!.nombres}`} <CopyButton
> copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`}
Nombres&nbsp; >
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i> <b>AP</b>
</CopyButton> </CopyButton>
<CopyButton <CopyButton
copyText={`${props.person!.apellidoPaterno} ${props.person!.apellidoMaterno}`} copyText={`${props.person!.apellidoPaterno}`}
> >
<b>Apellidos</b> <b><i>Ap</i></b>
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i> </CopyButton>
</CopyButton> <CopyButton
<CopyButton copyText={`${props.person!.apellidoMaterno}`}
copyText={`${props.person!.apellidoPaterno}`} >
> <b><i>Am</i></b>
<b><i>Paterno</i></b> </CopyButton>
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
</CopyButton>
<CopyButton
copyText={`${props.person!.apellidoMaterno}`}
>
<b><i>Materno</i></b>
<i class="ph ph-clipboard-text text-2xl align-middle ml-2"></i>
</CopyButton>
</div>
</div> </div>
<p <p
@ -102,7 +93,7 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
</p> </p>
<Show when={!loading()}> <Show when={!loading()}>
<table class="table-auto border border-c-outline my-4"> <table class="table-auto border border-c-outline my-4 w-full">
<thead> <thead>
<tr> <tr>
<th class="p-2">CURSO</th> <th class="p-2">CURSO</th>
@ -131,7 +122,6 @@ export function Registers(props: { person: Person | null, lastUpdate: number })
function Register(props: { cert: RegisterReturn, onUpdate: () => void }) { function Register(props: { cert: RegisterReturn, onUpdate: () => void }) {
const [deleteConfirmation, setDeleteConfirmation] = createSignal(false); const [deleteConfirmation, setDeleteConfirmation] = createSignal(false);
const [deleteText, setDeleteText] = createSignal("Eliminar");
const [downloading, setDownloading] = createSignal(false); const [downloading, setDownloading] = createSignal(false);
const deleteRegister = async() => { const deleteRegister = async() => {
@ -147,12 +137,10 @@ function Register(props: { cert: RegisterReturn, onUpdate: () => void }) {
} else { } else {
// Show delete confirmation // Show delete confirmation
setDeleteText("Estas seguro?");
setDeleteConfirmation(true); setDeleteConfirmation(true);
// Exit confirmation after 3 seconds // Exit confirmation after 3 seconds
setTimeout(() => { setTimeout(() => {
setDeleteText("Eliminar");
setDeleteConfirmation(false); setDeleteConfirmation(false);
}, 3000); }, 3000);
} }
@ -205,8 +193,8 @@ function Register(props: { cert: RegisterReturn, onUpdate: () => void }) {
return ( return (
<tr class="odd:bg-c-surface-variant"> <tr class="odd:bg-c-surface-variant">
<td class="py-2 px-4">{props.cert.curso_nombre}</td> <td class="py-2 px-4">{props.cert.curso_nombre}</td>
<td class="py-2 px-4 font-mono">{props.cert.fecha_inscripcion.toString()}</td> <td class="py-2 px-2 text-center font-mono">{props.cert.fecha_inscripcion.toString()}</td>
<td class="py-1 px-2"> <td class="py-1 px-2 text-center">
<CopyButton <CopyButton
copyText={certCode()} copyText={certCode()}
> >
@ -237,14 +225,14 @@ function Register(props: { cert: RegisterReturn, onUpdate: () => void }) {
<i class="ph ph-pen"></i> <i class="ph ph-pen"></i>
</button> </button>
</td> </td>
<td class="py-2 px-4"> <td class="py-2 px-2">
<button <button
class={`rounded-full py-1 px-2 shadow class={`rounded-full py-1 px-2 shadow transition-colors
${deleteConfirmation() ? "bg-c-error-container text-c-on-error-container" : "bg-c-error text-c-on-error"} ${deleteConfirmation() ? "bg-c-error-container text-c-on-error-container" : "bg-c-error text-c-on-error"}
cursor-pointer`} cursor-pointer`}
onclick={deleteRegister} onclick={deleteRegister}
> >
{deleteText()} <i class="ph ph-trash"></i>
</button> </button>
</td> </td>
</tr> </tr>

View File

@ -125,7 +125,7 @@ function InputBox(props: {
const copyToClipboard: HTMLButtonEvent = (ev) => { const copyToClipboard: HTMLButtonEvent = (ev) => {
ev.preventDefault(); ev.preventDefault();
if (props.dni.length == 8) { if (props.dni.length === 8) {
navigator.clipboard.writeText(props.dni); navigator.clipboard.writeText(props.dni);
setSuccessAnimation(true); setSuccessAnimation(true);
setTimeout(() => setSuccessAnimation(false), 1000); setTimeout(() => setSuccessAnimation(false), 1000);
@ -136,7 +136,7 @@ function InputBox(props: {
ev.preventDefault(); ev.preventDefault();
props.setDni(""); props.setDni("");
} };
return ( return (
<div class="grid gap-2 grid-cols-[10rem_3rem_3rem]"> <div class="grid gap-2 grid-cols-[10rem_3rem_3rem]">
@ -157,15 +157,15 @@ function InputBox(props: {
disabled={props.loading} disabled={props.loading}
/> />
<button <button
class={(successAnimation() ? "bg-c-success" : "bg-c-primary") + " rounded transition-colors"} class={`${successAnimation() ? "bg-c-success" : "bg-c-primary"} rounded transition-colors`}
onclick={copyToClipboard} onclick={copyToClipboard}
type="button" type="button"
> >
<i class={(successAnimation() ? "text-c-on-success" : "text-c-on-primary") + " ph ph-clipboard-text text-2xl align-middle"}></i> <i class={`${successAnimation() ? "text-c-on-success" : "text-c-on-primary"} ph ph-clipboard-text text-2xl align-middle`}></i>
</button> </button>
<button class="bg-c-error rounded" onclick={clearDni} type="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> <i class="ph ph-trash text-2xl text-c-on-primary align-middle"></i>
</button> </button>
</div> </div>
) );
} }

View File

@ -0,0 +1,11 @@
/**
* This file generates a hidration script, which must
* then be sent to the client.
*/
import {hydrate} from "solid-js/web";
import { AulaVirtual } from "../AulaVirtual";
const root = document.getElementById("root")!;
hydrate(AulaVirtual, root);