[FE] Begin interface for Aulavirtual
This commit is contained in:
parent
87bf255635
commit
5a8b93edc2
4
.gitignore
vendored
4
.gitignore
vendored
@ -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
|
||||||
|
@ -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",
|
||||||
|
});
|
||||||
|
25
esbuild.js
25
esbuild.js
@ -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...");
|
||||||
|
})();
|
||||||
|
@ -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")!);
|
||||||
|
@ -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 {}
|
||||||
|
@ -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({
|
||||||
|
13
src/controller/aulavirtual/aulavirtual.controller.ts
Normal file
13
src/controller/aulavirtual/aulavirtual.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
28
src/controller/aulavirtual/aulavirtual.template.ts
Normal file
28
src/controller/aulavirtual/aulavirtual.template.ts
Normal 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
64
src/views/AulaVirtual.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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()}
|
||||||
|
@ -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
|
>
|
||||||
<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>
|
||||||
|
@ -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]">
|
||||||
@ -156,16 +156,16 @@ function InputBox(props: {
|
|||||||
onChange={(e) => props.setDni(e.target.value)}
|
onChange={(e) => props.setDni(e.target.value)}
|
||||||
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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
11
src/views/hydration/hydration_aulavirtual.ts
Normal file
11
src/views/hydration/hydration_aulavirtual.ts
Normal 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);
|
Loading…
Reference in New Issue
Block a user