Vista de seleccion de cursos

master
Araozu 2022-10-14 09:52:47 -05:00
parent 16f2e71430
commit 57a96ce28c
13 changed files with 546 additions and 44 deletions

217
API.md Normal file
View File

@ -0,0 +1,217 @@
# Inicio de sesion
```ts
// HTTP POST
// Url: /login
// Frontend envia el correo del usuario
{
correo_usuario: string
}
// Backend responde con una lista de matriculas
// Si el correo es valido y el usuario tiene alguna matricula
{
matriculas: Array<number> // Un array de id_laboratorio
}
// Si el correo es valido pero el usuario no tiene matriculas
{
matriculas: [] // Un array vacio
}
// Si el correo es invalido: Se envia HTTP 401
```
# Lista cursos
```ts
// HTTP GET
// Url: /cursos
// El frontend pide una lista de la informacion de todos los cursos
// Backend responde con una lista de todos los cursos
[
{
id_curso: number,
id_datos_carrera: any, // Opcional
nombre_curso: string,
curso_anio: number | string, // Numero o string, dependiendo de como este en DB
abreviado: string,
}
]
```
# Carga de horarios
```ts
// HTTP GET
// Url: /horarios?cursos=...
// El frontend envia una lista de cursos, de los cuales recuperar sus datos
{
cursos: Array<number> // Un array de id_curso
}
// Backend responde con los cursos especificados y sus horarios
[
// Cada objeto dentro del array sera un Curso
{
id_curso: number,
id_datos_carrera: any, // Opcional
nombre_curso: string,
curso_anio: number | string,
abreviado: string,
// Un array de objetos, estos objetos son de la entidad Laboratorio
laboratorios: [
{
id_laboratorio: number,
id_curso: number,
id_horario: number,
grupo: string,
docente: string,
// Array de objetos de la entidad Horario
horario: [
{
id_horario: number,
dia: string,
hora_inicio: string,
hora_fin: string,
}
]
}
]
}
]
```
# Matricula
```ts
// HTTP POST
// Url: /matricula
// Frontend envia una lista de horarios en los cuales se matricula y el usuario
{
correo_usuario: string, // Correo del usuario
horarios: Array<number> // Array de id_laboratorio
}
// Backend devuelve HTTP 200 si exitoso, HTTP 400 si hay error (no hay cupos)
```
# Recuperacion de matricula
```ts
// HTTP POST
// Url: /recuperacion
// Frontend envia una lista de id_laboratorio
{
matriculas: Array<number> // Array de id_laboratorio
}
// Backend devuelve:
// Por cada id_laboratorio: datos del curso y el laboratorio
[
// Un objeto asi por cada id_laboratorio
{
nombre_curso: string, // Este es el campo `nombre_curso` de la entidad Curso
grupo: string, // Campo `grupo` de la entidad Laboratorio
docente: string, // Campo `docente` de la entidad Laboratorio
}
]
```
# Creacion de datos
Endpoints para insertar datos a la db
```ts
// POST /crear-carrera
// Front
{
nombre_carrera: string
}
// Backend
{
id_carrera: number // id de la carrera creada
}
```
```ts
// POST /crear-curso
// Front
{
id_datos_carrera: number,
nombre_curso: string,
curso_anio: string, // Ejm: "1er", "2do", etc
abreviado: string
}
// Backend
{
id_curso: number // id del curso creado
}
```
```ts
// POST /crear-laboratorio
// Front
{
id_curso: number,
grupo: string, // Una letra
docente: string,
max_estudiantes: number
}
// Backend
{
id_laboratorio: number // id del lab creado
}
```
```ts
// POST /crear-horario
// Front
{
id_laboratorio: number,
dia: string, // "Lunes", "Martes", etc
hora_inicio: string, // "07:00"
hora_fin: string, // "08:50"
}
// Backend
{
id_horario: number // id del horario creado
}
```

5
jest.config.js Normal file
View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

82
src/API/ListaCursos.ts Normal file
View File

@ -0,0 +1,82 @@
/*
// HTTP GET
// Url: /cursos
// El frontend pide una lista de la informacion de todos los cursos
// Backend responde con una lista de todos los cursos
[
{
id_curso: number,
id_datos_carrera: any, // Opcional
nombre_curso: string,
curso_anio: number | string, // Numero o string, dependiendo de como este en DB
abreviado: string,
}
]
*/
import { SERVER_PATH } from "../Store";
export type InfoCurso = {
id_curso: number,
nombre_curso: string,
curso_anio: string,
abreviado: string,
}
// `"1er"`, `"2do"`, etc
type NombreAnio = string
export type RespuestaListaCursos = {[key: NombreAnio]: Array<InfoCurso>}
type ListaCursosFn = () => Promise<RespuestaListaCursos>
export const getAllListaCursos: ListaCursosFn = async() => {
const response = await fetch(`${SERVER_PATH}/cursos`);
const data = await response.json() as Array<InfoCurso>;
const resultMap: RespuestaListaCursos = {};
data.forEach((curso) => {
if (resultMap[curso.curso_anio] === undefined) {
resultMap[curso.curso_anio] = [];
}
resultMap[curso.curso_anio]?.push(curso);
});
return resultMap;
};
export const getAllListaCursosMock: ListaCursosFn = async() => {
const arr5to: Array<InfoCurso> = [
{
id_curso: 0,
nombre_curso: "Gestion de Sistemas y Tecnologias de Informacion",
curso_anio: "5to",
abreviado: "GSTI",
},
{
id_curso: 1,
nombre_curso: "Practicas Pre Profesionales",
curso_anio: "5to",
abreviado: "PPP",
},
];
const arr4to: Array<InfoCurso> = [
{
id_curso: 2,
nombre_curso: "Diseño y Arquitectura de Software",
curso_anio: "4to",
abreviado: "DAS",
},
{
id_curso: 3,
nombre_curso: "Gestion de Proyectos de Software",
curso_anio: "4to",
abreviado: "GPS",
},
];
return {
"5to": arr5to,
"4to": arr4to,
};
};

View File

@ -5,6 +5,7 @@ import { useRouter } from "./Router";
import { Switch, Match, Show } from "solid-js"; import { Switch, Match, Show } from "solid-js";
import { Wallpaper } from "./Wallpaper"; import { Wallpaper } from "./Wallpaper";
import { SistemasMovil } from "./Views/SistemasMovil"; import { SistemasMovil } from "./Views/SistemasMovil";
import { SeleccionCursos } from "./Views/SeleccionCursos";
function App() { function App() {
const route = useRouter(); const route = useRouter();
@ -22,6 +23,9 @@ function App() {
<Match when={route() === "/editor/"}> <Match when={route() === "/editor/"}>
<Editor /> <Editor />
</Match> </Match>
<Match when={route() === "/seleccion-cursos/"}>
<SeleccionCursos />
</Match>
<Match when={route() === "/sistemas-movil/"}> <Match when={route() === "/sistemas-movil/"}>
<SistemasMovil /> <SistemasMovil />
</Match> </Match>

View File

@ -28,6 +28,7 @@ export const horasDescanso = [
"15:40 - 15:50", "15:40 - 15:50",
"17:30 - 17:40", "17:30 - 17:40",
]; ];
export const SERVER_PATH = "";
const numImgGuardado = Number(localStorage.getItem("num-img") ?? "0"); const numImgGuardado = Number(localStorage.getItem("num-img") ?? "0");
const tamanoLetraGuardado = Number(/* localStorage.getItem("tamano-letra") ?? */ "16"); const tamanoLetraGuardado = Number(/* localStorage.getItem("tamano-letra") ?? */ "16");

View File

@ -3,6 +3,7 @@ import { StyleSheet, css } from "aphrodite/no-important";
import { RouterLink } from "../Router"; import { RouterLink } from "../Router";
import { Show } from "solid-js"; import { Show } from "solid-js";
import { isMobile } from "../Store"; import { isMobile } from "../Store";
import { MobileIndex } from "./MobileIndex";
const e = StyleSheet.create({ const e = StyleSheet.create({
contenedorGlobal: { contenedorGlobal: {
@ -31,46 +32,7 @@ const e = StyleSheet.create({
}, },
}); });
function MobileIndex() {
const s = StyleSheet.create({
boton: {
backgroundColor: "var(--color-primario)",
color: "white",
padding: "1rem 5rem",
borderRadius: "25px",
margin: "1.5rem 0",
boxShadow: "2px 2px 2px 0 gray",
cursor: "pointer",
},
entrada: {
borderTop: "none",
borderRight: "none",
borderLeft: "none",
borderBottom: "solid 2px gray",
padding: "0.75rem 1rem",
},
});
const inputElement = <input type="email" placeholder="Correo electronico" className={css(s.entrada)} />;
const login = () => {
console.log((inputElement as HTMLInputElement).value);
window.location.href = "#/sistemas-movil/";
};
return (
<div className={css(e.contenedorGlobal)}>
<div style="text-align: center;">
<h1>Iniciar sesión</h1>
<br />
<br />
{inputElement}
<br />
<button className={css(s.boton)} onClick={login}>Iniciar Sesion</button>
</div>
</div>
);
}
export function Index() { export function Index() {
return ( return (

113
src/Views/MobileIndex.tsx Normal file
View File

@ -0,0 +1,113 @@
import { css, StyleSheet } from "aphrodite/no-important";
import { createSignal } from "solid-js";
import { SERVER_PATH } from "../Store";
const e = StyleSheet.create({
contenedorGlobal: {
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
cont: {
width: "30rem",
},
parrafo: {
textAlign: "justify",
lineHeight: "1.4rem",
},
botonAccion: {
width: "30rem",
display: "inline-block",
textAlign: "center",
},
iconoGitHub: {
fontSize: "1.25rem",
verticalAlign: "bottom",
marginRight: "0.5rem",
},
});
type IdLaboratorio = number;
type LoginData = {correo_usuario: string};
type LoginResponse = Promise<{matriculas: Array<IdLaboratorio>} | null>;
type LoginFunction = (data: LoginData) => LoginResponse;
// Mock for a login without courses
const mockLoginEmpty: LoginFunction = async(data) => ({matriculas: []});
// Mock for a login with courses
const mockLoginNotEmpty: LoginFunction = async(_) => ({
matriculas: [0, 1, 2, 3],
});
// Error login mock
const mockLoginWithError: LoginFunction = async(_) => null;
// Standard login
const loginFn: LoginFunction = async(data) => {
const petition = await fetch(`${SERVER_PATH}/login`, {
method: "POST",
body: JSON.stringify({
correo_usuario: data.correo_usuario,
}),
});
if (!petition.ok) return null;
return await petition.json() as {matriculas: Array<IdLaboratorio>};
};
export function MobileIndex() {
const s = StyleSheet.create({
boton: {
backgroundColor: "var(--color-primario)",
color: "white",
padding: "1rem 5rem",
borderRadius: "25px",
margin: "1.5rem 0",
boxShadow: "2px 2px 2px 0 gray",
cursor: "pointer",
},
entrada: {
borderTop: "none",
borderRight: "none",
borderLeft: "none",
borderBottom: "solid 2px gray",
padding: "0.75rem 1rem",
},
});
const [msgErrorVisible, setMsgErrorVisible] = createSignal(false);
const inputElement = <input required type="email" placeholder="Correo electronico" className={css(s.entrada)} />;
const login = async(ev: Event) => {
ev.preventDefault();
const email = (inputElement as HTMLInputElement).value;
const response = await mockLoginEmpty({correo_usuario: email});
if (response === null) {
setMsgErrorVisible(true);
setTimeout(() => setMsgErrorVisible(false), 2500);
} else if (response.matriculas.length === 0) {
window.location.href = "#/seleccion-cursos/";
} else if (response.matriculas.length > 0) {
alert("TODO");
}
};
return (
<div className={css(e.contenedorGlobal)}>
<div style="text-align: center;">
<h1>Iniciar sesión</h1>
<br />
<br />
<form onSubmit={(ev) => login(ev)}>
{inputElement}
<br />
<button type="submit" className={css(s.boton)}>Iniciar Sesion</button>
</form>
<span style={{opacity: msgErrorVisible() ? 1 : 0}}>El correo es invalido</span>
</div>
</div>
);
}

View File

@ -0,0 +1,79 @@
import { TopBar } from "./SistemasMovil/TopBar";
import { StyleSheet, css } from "aphrodite/no-important";
import { Card } from "../components/Card";
import { createSignal, For } from "solid-js";
import { getAllListaCursosMock, RespuestaListaCursos } from "../API/ListaCursos";
import { Button } from "../components/Button";
const s = StyleSheet.create({
checkbox: {
width: "1.25rem",
height: "1.25rem",
margin: "0 0.5rem",
},
grid: {
display: "grid",
gridTemplateColumns: "3rem auto",
gridRowGap: "1rem",
},
});
export function SeleccionCursos() {
const [cursos, setCursos] = createSignal<RespuestaListaCursos>({});
// Recuperar cursos de back
(async() => setCursos(await getAllListaCursosMock()))();
const submit = (ev: Event) => {
ev.preventDefault();
const form = ev.target as HTMLFormElement;
// Los checkboxes
const elements = form.elements;
const idsAEnviar: Array<number> = [];
for (let i = 0; i < elements.length; i += 1) {
const inputBox = elements[i] as HTMLInputElement;
if (inputBox.checked) {
idsAEnviar.push(parseInt(inputBox.value, 10));
}
}
console.log(idsAEnviar);
};
return (
<div>
<TopBar tituloBarra="Selección de Cursos" />
<Card>
<p>Escoge los cursos en los que matricularte</p>
</Card>
<form onSubmit={(ev) => submit(ev)}>
<For each={Object.entries(cursos())}>
{([nombreAnio, infoCurso]) => (
<Card>
<h2>{nombreAnio} año</h2>
<div className={css(s.grid)}>
<For each={infoCurso}>
{(curso) => (
<>
<input
type="checkbox"
value={curso.id_curso}
className={css(s.checkbox)}
/>
<span>{curso.nombre_curso}</span>
</>
)}
</For>
</div>
</Card>
)}
</For>
<div style="text-align: center">
<Button texto={"Continuar"} />
</div>
</form>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { Table } from "./SistemasMovil/Table";
export function SistemasMovil() { export function SistemasMovil() {
return ( return (
<div> <div>
<TopBar /> <TopBar tituloBarra="Mi Horario" />
<Table /> <Table />
</div> </div>
); );

View File

@ -1,11 +1,11 @@
import {StyleSheet, css} from "aphrodite/no-important"; import {StyleSheet, css} from "aphrodite/no-important";
import { createSignal, For, JSX } from "solid-js"; import { createSignal, For } from "solid-js";
import { horas } from "../../Store"; import { horas } from "../../Store";
const s = StyleSheet.create({ const s = StyleSheet.create({
container: { container: {
display: "grid", display: "grid",
gridTemplateColumns: "14vw 1fr 1fr", gridTemplateColumns: "13vw 1fr 1fr",
textAlign: "center", textAlign: "center",
fontSize: "0.9rem", fontSize: "0.9rem",
}, },

View File

@ -25,7 +25,7 @@ const s = StyleSheet.create({
}, },
}); });
export function TopBar() { export function TopBar(props: {tituloBarra: string}) {
return ( return (
<nav className={css(s.bar)}> <nav className={css(s.bar)}>
<button> <button>
@ -34,7 +34,7 @@ export function TopBar() {
title={"Cambiar imagen de fondo"} title={"Cambiar imagen de fondo"}
/> />
</button> </button>
<p className={css(s.barLabel)}>Mi horario</p> <p className={css(s.barLabel)}>{props.tituloBarra}</p>
</nav> </nav>
); );
} }

20
src/components/Button.tsx Normal file
View File

@ -0,0 +1,20 @@
import { StyleSheet, css } from "aphrodite/no-important";
export function Button(props: {texto: string}) {
const s = StyleSheet.create({
boton: {
backgroundColor: "var(--color-primario)",
color: "white",
padding: "1rem 5rem",
borderRadius: "25px",
margin: "1.5rem 0",
boxShadow: "2px 2px 2px 0 gray",
cursor: "pointer",
},
});
return (
<button type="submit" className={css(s.boton)}>
{props.texto}
</button>
);
}

19
src/components/Card.tsx Normal file
View File

@ -0,0 +1,19 @@
import { JSX } from "solid-js";
import { StyleSheet, css } from "aphrodite/no-important";
const s = StyleSheet.create({
card: {
padding: "0.5rem",
border: "solid 2px var(--color-borde)",
borderRadius: "10px",
margin: "0.5rem",
},
});
export function Card(props: {children?: JSX.Element}) {
return (
<div className={css(s.card)}>
{props.children}
</div>
);
}