Usar rutas de backend
This commit is contained in:
parent
94a851b188
commit
cb7e982c21
@ -21,6 +21,7 @@ export const loginFn: LoginFunction = async(data) => {
|
|||||||
const petition = await fetch(`${SERVER_PATH}/login`, {
|
const petition = await fetch(`${SERVER_PATH}/login`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -15,7 +15,7 @@ function App() {
|
|||||||
const isMobile = screen.width <= 500;
|
const isMobile = screen.width <= 500;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App" style={isMobile ? "--color-texto: #202020;" : ""}>
|
<div class="App" style={isMobile ? "--color-texto: #202020;" : ""}>
|
||||||
<Show when={!isMobile}>
|
<Show when={!isMobile}>
|
||||||
<Wallpaper />
|
<Wallpaper />
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -29,7 +29,7 @@ export const horasDescanso = [
|
|||||||
"15:40 - 15:50",
|
"15:40 - 15:50",
|
||||||
"17:30 - 17:40",
|
"17:30 - 17:40",
|
||||||
];
|
];
|
||||||
export const SERVER_PATH = "";
|
export const SERVER_PATH = "http://localhost:4000/sistema";
|
||||||
|
|
||||||
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");
|
||||||
|
@ -4,7 +4,7 @@ import { RouterLink } from "../Router";
|
|||||||
import { batch, createSignal, Show } from "solid-js";
|
import { batch, createSignal, Show } from "solid-js";
|
||||||
import { isMobile, setGruposSeleccionados } from "../Store";
|
import { isMobile, setGruposSeleccionados } from "../Store";
|
||||||
import { MobileIndex } from "./MobileIndex";
|
import { MobileIndex } from "./MobileIndex";
|
||||||
import { mockLoginEmpty, mockLoginNotEmpty, mockLoginWithError } from "../API/Login";
|
import { loginFn } from "../API/Login";
|
||||||
|
|
||||||
const e = StyleSheet.create({
|
const e = StyleSheet.create({
|
||||||
contenedorGlobal: {
|
contenedorGlobal: {
|
||||||
@ -46,17 +46,17 @@ const e = StyleSheet.create({
|
|||||||
|
|
||||||
export function Index() {
|
export function Index() {
|
||||||
const [msgErrorVisible, setMsgErrorVisible] = createSignal(false);
|
const [msgErrorVisible, setMsgErrorVisible] = createSignal(false);
|
||||||
const inputElement = <input className={css(e.inputCorreo)} type="email" required placeholder="correo@unsa.edu.pe" />;
|
const inputElement = <input class={css(e.inputCorreo)} type="email" required placeholder="correo@unsa.edu.pe" />;
|
||||||
|
|
||||||
const login = async(ev: Event) => {
|
const login = async(ev: Event) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const email = (inputElement as HTMLInputElement).value;
|
const email = (inputElement as HTMLInputElement).value;
|
||||||
const response = await mockLoginEmpty({correo_usuario: email});
|
const response = await loginFn({correo_usuario: email});
|
||||||
|
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
setMsgErrorVisible(true);
|
setMsgErrorVisible(true);
|
||||||
setTimeout(() => setMsgErrorVisible(false), 2500);
|
setTimeout(() => setMsgErrorVisible(false), 2500);
|
||||||
} else if (response.matriculas.length === 0) {
|
} else if (!response.matriculas || response.matriculas.length === 0) {
|
||||||
localStorage.setItem("correo", email);
|
localStorage.setItem("correo", email);
|
||||||
window.location.href = "#/pc/seleccion-cursos/";
|
window.location.href = "#/pc/seleccion-cursos/";
|
||||||
} else if (response.matriculas.length > 0) {
|
} else if (response.matriculas.length > 0) {
|
||||||
@ -73,9 +73,9 @@ export function Index() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={!isMobile()}>
|
<Show when={!isMobile()}>
|
||||||
<div className={css(e.contenedorGlobal)}>
|
<div class={css(e.contenedorGlobal)}>
|
||||||
<div className={css(e.cont)}>
|
<div class={css(e.cont)}>
|
||||||
<div className={css(estilosGlobales.contenedor, estilosGlobales.inlineBlock, e.cont)}>
|
<div class={css(estilosGlobales.contenedor, estilosGlobales.inlineBlock, e.cont)}>
|
||||||
<h1 style={{
|
<h1 style={{
|
||||||
"text-align": "center",
|
"text-align": "center",
|
||||||
"font-size": "1.75rem",
|
"font-size": "1.75rem",
|
||||||
@ -83,7 +83,7 @@ export function Index() {
|
|||||||
>
|
>
|
||||||
Horarios UNSA
|
Horarios UNSA
|
||||||
</h1>
|
</h1>
|
||||||
<p className={css(e.parrafo)}>
|
<p class={css(e.parrafo)}>
|
||||||
Inicia sesión con tu correo institucional.
|
Inicia sesión con tu correo institucional.
|
||||||
<br />
|
<br />
|
||||||
{inputElement}
|
{inputElement}
|
||||||
@ -92,17 +92,17 @@ export function Index() {
|
|||||||
El correo es invalido
|
El correo es invalido
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={login} className={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, e.botonAccion)}>
|
<button onClick={login} class={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, e.botonAccion)}>
|
||||||
Iniciar sesion
|
Iniciar sesion
|
||||||
</button>
|
</button>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a
|
<a
|
||||||
className={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, e.botonAccion)}
|
class={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, e.botonAccion)}
|
||||||
href="https://github.com/Araozu/horarios-unsa-2/"
|
href="https://github.com/Araozu/horarios-unsa-2/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<i className={`${css(e.iconoGitHub)} ph-code`} />
|
<i class={`${css(e.iconoGitHub)} ph-code`} />
|
||||||
Código fuente en GitHub
|
Código fuente en GitHub
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { css, StyleSheet } from "aphrodite/no-important";
|
import { css, StyleSheet } from "aphrodite/no-important";
|
||||||
import { batch, createSignal } from "solid-js";
|
import { batch, createSignal } from "solid-js";
|
||||||
import { SERVER_PATH, setGruposSeleccionados } from "../Store";
|
import { SERVER_PATH, setGruposSeleccionados } from "../Store";
|
||||||
import { mockLoginEmpty } from "../API/Login";
|
import { loginFn } from "../API/Login";
|
||||||
|
|
||||||
const e = StyleSheet.create({
|
const e = StyleSheet.create({
|
||||||
contenedorGlobal: {
|
contenedorGlobal: {
|
||||||
@ -51,12 +51,12 @@ export function MobileIndex() {
|
|||||||
});
|
});
|
||||||
const [msgErrorVisible, setMsgErrorVisible] = createSignal(false);
|
const [msgErrorVisible, setMsgErrorVisible] = createSignal(false);
|
||||||
|
|
||||||
const inputElement = <input required type="email" placeholder="correo@unsa.edu.pe" className={css(s.entrada)} />;
|
const inputElement = <input required type="email" placeholder="correo@unsa.edu.pe" class={css(s.entrada)} />;
|
||||||
|
|
||||||
const login = async(ev: Event) => {
|
const login = async(ev: Event) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const email = (inputElement as HTMLInputElement).value;
|
const email = (inputElement as HTMLInputElement).value;
|
||||||
const response = await mockLoginEmpty({correo_usuario: email});
|
const response = await loginFn({correo_usuario: email});
|
||||||
|
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
setMsgErrorVisible(true);
|
setMsgErrorVisible(true);
|
||||||
@ -76,7 +76,7 @@ export function MobileIndex() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css(e.contenedorGlobal)}>
|
<div class={css(e.contenedorGlobal)}>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<h1>Iniciar sesión</h1>
|
<h1>Iniciar sesión</h1>
|
||||||
<br />
|
<br />
|
||||||
@ -84,7 +84,7 @@ export function MobileIndex() {
|
|||||||
<form onSubmit={(ev) => login(ev)}>
|
<form onSubmit={(ev) => login(ev)}>
|
||||||
{inputElement}
|
{inputElement}
|
||||||
<br />
|
<br />
|
||||||
<button type="submit" className={css(s.boton)}>Iniciar Sesion</button>
|
<button type="submit" class={css(s.boton)}>Iniciar Sesion</button>
|
||||||
</form>
|
</form>
|
||||||
<span style={{opacity: msgErrorVisible() ? 1 : 0}}>El correo es invalido</span>
|
<span style={{opacity: msgErrorVisible() ? 1 : 0}}>El correo es invalido</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import { TopBar } from "./SistemasMovil/TopBar";
|
|||||||
import { StyleSheet, css } from "aphrodite/no-important";
|
import { StyleSheet, css } from "aphrodite/no-important";
|
||||||
import { Card } from "../components/Card";
|
import { Card } from "../components/Card";
|
||||||
import { createSignal, For } from "solid-js";
|
import { createSignal, For } from "solid-js";
|
||||||
import { getAllListaCursosMock, RespuestaListaCursos } from "../API/ListaCursos";
|
import { getAllListaCursos, RespuestaListaCursos } from "../API/ListaCursos";
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
|
|
||||||
const s = StyleSheet.create({
|
const s = StyleSheet.create({
|
||||||
@ -23,7 +23,7 @@ export function SeleccionCursos() {
|
|||||||
const [msjErr, setMsjError] = createSignal(false);
|
const [msjErr, setMsjError] = createSignal(false);
|
||||||
|
|
||||||
// Recuperar cursos de back
|
// Recuperar cursos de back
|
||||||
(async() => setCursos(await getAllListaCursosMock()))();
|
(async() => setCursos(await getAllListaCursos()))();
|
||||||
|
|
||||||
const submit = (ev: Event) => {
|
const submit = (ev: Event) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@ -62,14 +62,14 @@ export function SeleccionCursos() {
|
|||||||
{([nombreAnio, infoCurso]) => (
|
{([nombreAnio, infoCurso]) => (
|
||||||
<Card>
|
<Card>
|
||||||
<h2>{nombreAnio} año</h2>
|
<h2>{nombreAnio} año</h2>
|
||||||
<div className={css(s.grid)}>
|
<div class={css(s.grid)}>
|
||||||
<For each={infoCurso}>
|
<For each={infoCurso}>
|
||||||
{(curso) => (
|
{(curso) => (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
value={curso.id_curso}
|
value={curso.id_curso}
|
||||||
className={css(s.checkbox)}
|
class={css(s.checkbox)}
|
||||||
/>
|
/>
|
||||||
<span>{curso.nombre_curso}</span>
|
<span>{curso.nombre_curso}</span>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { TopBar } from "./SistemasMovil/TopBar";
|
import { TopBar } from "./SistemasMovil/TopBar";
|
||||||
import { GrupoDia, Table, TableInput } from "./SistemasMovil/Table";
|
import { GrupoDia, Table, TableInput } from "./SistemasMovil/Table";
|
||||||
import { getHorariosMock, Horario, ListaCursosCompleto } from "../API/CargaHorarios";
|
import { getHorarios, Horario, ListaCursosCompleto } from "../API/CargaHorarios";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { generarMapaCeldas } from "./SistemasMovil/mapaCeldas";
|
import { generarMapaCeldas } from "./SistemasMovil/mapaCeldas";
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
@ -12,7 +12,7 @@ export function SistemasMovil() {
|
|||||||
// Obtener cursos seleccionados del servidor
|
// Obtener cursos seleccionados del servidor
|
||||||
(async() => {
|
(async() => {
|
||||||
const cursos: Array<string> = JSON.parse(localStorage.getItem("cursos-seleccionados") ?? "[]");
|
const cursos: Array<string> = JSON.parse(localStorage.getItem("cursos-seleccionados") ?? "[]");
|
||||||
const data = await getHorariosMock({
|
const data = await getHorarios({
|
||||||
cursos: cursos.map((x) => parseInt(x, 10)),
|
cursos: cursos.map((x) => parseInt(x, 10)),
|
||||||
});
|
});
|
||||||
setRawData(data);
|
setRawData(data);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { TopBar } from "./SistemasMovil/TopBar";
|
import { TopBar } from "./SistemasMovil/TopBar";
|
||||||
import { Card } from "../components/Card";
|
import { Card } from "../components/Card";
|
||||||
import { createSignal, For } from "solid-js";
|
import { createSignal, For } from "solid-js";
|
||||||
import { getMatriculaMock, InfoMatricula } from "../API/VerMatricula";
|
import { getMatricula, InfoMatricula } from "../API/VerMatricula";
|
||||||
import { gruposSeleccionados } from "../Store";
|
import { gruposSeleccionados } from "../Store";
|
||||||
|
|
||||||
export function VerMatricula() {
|
export function VerMatricula() {
|
||||||
@ -11,7 +11,7 @@ export function VerMatricula() {
|
|||||||
const laboratorios = Object.entries(gruposSeleccionados)
|
const laboratorios = Object.entries(gruposSeleccionados)
|
||||||
.filter((x) => x[1] === true)
|
.filter((x) => x[1] === true)
|
||||||
.map((x) => parseInt(x[0], 10));
|
.map((x) => parseInt(x[0], 10));
|
||||||
setInfoMatriculas(await getMatriculaMock({matriculas: laboratorios}));
|
setInfoMatriculas(await getMatricula({matriculas: laboratorios}));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { css, StyleSheet } from "aphrodite/no-important";
|
import { css, StyleSheet } from "aphrodite/no-important";
|
||||||
import { estilosGlobales } from "../../Estilos";
|
import { estilosGlobales } from "../../Estilos";
|
||||||
import { createSignal, For } from "solid-js";
|
import { createSignal, For } from "solid-js";
|
||||||
import { getAllListaCursosMock, RespuestaListaCursos } from "../../API/ListaCursos";
|
import { getAllListaCursos, RespuestaListaCursos } from "../../API/ListaCursos";
|
||||||
|
|
||||||
const e = StyleSheet.create({
|
const e = StyleSheet.create({
|
||||||
contenedorGlobal: {
|
contenedorGlobal: {
|
||||||
@ -55,7 +55,7 @@ export function SeleccionCursos() {
|
|||||||
const [msgErr, setMsgError] = createSignal(false);
|
const [msgErr, setMsgError] = createSignal(false);
|
||||||
|
|
||||||
// Recuperar cursos de back
|
// Recuperar cursos de back
|
||||||
(async() => setCursos(await getAllListaCursosMock()))();
|
(async() => setCursos(await getAllListaCursos()))();
|
||||||
|
|
||||||
const submit = (ev: Event) => {
|
const submit = (ev: Event) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
@ -83,10 +83,10 @@ export function SeleccionCursos() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css(e.contenedorGlobal)}>
|
<div class={css(e.contenedorGlobal)}>
|
||||||
<div className={css(e.cont)}>
|
<div class={css(e.cont)}>
|
||||||
<form onSubmit={submit}>
|
<form onSubmit={submit}>
|
||||||
<div className={css(estilosGlobales.contenedor, estilosGlobales.inlineBlock, e.cont)}>
|
<div class={css(estilosGlobales.contenedor, estilosGlobales.inlineBlock, e.cont)}>
|
||||||
<h1 style={{
|
<h1 style={{
|
||||||
"text-align": "center",
|
"text-align": "center",
|
||||||
"font-size": "1.75rem",
|
"font-size": "1.75rem",
|
||||||
@ -100,14 +100,14 @@ export function SeleccionCursos() {
|
|||||||
{([nombreAnio, infoCurso]) => (
|
{([nombreAnio, infoCurso]) => (
|
||||||
<>
|
<>
|
||||||
<h2>{nombreAnio} año</h2>
|
<h2>{nombreAnio} año</h2>
|
||||||
<div className={css(e.grid)}>
|
<div class={css(e.grid)}>
|
||||||
<For each={infoCurso}>
|
<For each={infoCurso}>
|
||||||
{(curso) => (
|
{(curso) => (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
value={curso.id_curso}
|
value={curso.id_curso}
|
||||||
className={css(e.checkbox)}
|
class={css(e.checkbox)}
|
||||||
/>
|
/>
|
||||||
<span>{curso.nombre_curso}</span>
|
<span>{curso.nombre_curso}</span>
|
||||||
</>
|
</>
|
||||||
@ -124,9 +124,9 @@ export function SeleccionCursos() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, e.botonAccion)}
|
class={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, e.botonAccion)}
|
||||||
>
|
>
|
||||||
Iniciar sesion
|
Continuar
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ import { ContenedorHorarios } from "./Sistemas/ContenedorHorarios";
|
|||||||
import { Creditos } from "../../Creditos";
|
import { Creditos } from "../../Creditos";
|
||||||
import { Separador } from "../../Separador";
|
import { Separador } from "../../Separador";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
import { getHorariosMock, ListaCursosCompleto } from "../../API/CargaHorarios";
|
import { getHorarios, ListaCursosCompleto } from "../../API/CargaHorarios";
|
||||||
import { Cursos, DatosGrupo } from "../../types/DatosHorario";
|
import { Cursos, DatosGrupo } from "../../types/DatosHorario";
|
||||||
import { infoDiaAListaHoras } from "../SistemasMovil";
|
import { infoDiaAListaHoras } from "../SistemasMovil";
|
||||||
import { StyleSheet, css } from "aphrodite/no-important";
|
import { StyleSheet, css } from "aphrodite/no-important";
|
||||||
@ -25,7 +25,7 @@ export function Sistemas() {
|
|||||||
// Obtener cursos seleccionados del servidor
|
// Obtener cursos seleccionados del servidor
|
||||||
(async() => {
|
(async() => {
|
||||||
const cursos: Array<string> = JSON.parse(localStorage.getItem("cursos-seleccionados") ?? "[]");
|
const cursos: Array<string> = JSON.parse(localStorage.getItem("cursos-seleccionados") ?? "[]");
|
||||||
const data = await getHorariosMock({
|
const data = await getHorarios({
|
||||||
cursos: cursos.map((x) => parseInt(x, 10)),
|
cursos: cursos.map((x) => parseInt(x, 10)),
|
||||||
});
|
});
|
||||||
setData(listaCursosADatos(data));
|
setData(listaCursosADatos(data));
|
||||||
@ -62,7 +62,7 @@ export function Sistemas() {
|
|||||||
<Separador />
|
<Separador />
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<button
|
<button
|
||||||
className={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, s.botonAccion)}
|
class={css(estilosGlobales.contenedor, estilosGlobales.contenedorCursor, s.botonAccion)}
|
||||||
onclick={matricular}
|
onclick={matricular}
|
||||||
>
|
>
|
||||||
Matricular
|
Matricular
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { css, StyleSheet } from "aphrodite/no-important";
|
import { css, StyleSheet } from "aphrodite/no-important";
|
||||||
import { estilosGlobales } from "../../Estilos";
|
import { estilosGlobales } from "../../Estilos";
|
||||||
import { createSignal, For } from "solid-js";
|
import { createSignal, For } from "solid-js";
|
||||||
import { getAllListaCursosMock, RespuestaListaCursos } from "../../API/ListaCursos";
|
import { getMatricula, InfoMatricula } from "../../API/VerMatricula";
|
||||||
import { getMatriculaMock, InfoMatricula } from "../../API/VerMatricula";
|
|
||||||
import { gruposSeleccionados } from "../../Store";
|
import { gruposSeleccionados } from "../../Store";
|
||||||
|
|
||||||
const e = StyleSheet.create({
|
const e = StyleSheet.create({
|
||||||
@ -59,14 +58,14 @@ export function VerMatricula() {
|
|||||||
const laboratorios = Object.entries(gruposSeleccionados)
|
const laboratorios = Object.entries(gruposSeleccionados)
|
||||||
.filter((x) => x[1] === true)
|
.filter((x) => x[1] === true)
|
||||||
.map((x) => parseInt(x[0], 10));
|
.map((x) => parseInt(x[0], 10));
|
||||||
setInfoMatriculas(await getMatriculaMock({matriculas: laboratorios}));
|
setInfoMatriculas(await getMatricula({matriculas: laboratorios}));
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css(e.contenedorGlobal)}>
|
<div class={css(e.contenedorGlobal)}>
|
||||||
<div className={css(e.cont)}>
|
<div class={css(e.cont)}>
|
||||||
|
|
||||||
<div className={css(estilosGlobales.contenedor, estilosGlobales.inlineBlock, e.cont)}>
|
<div class={css(estilosGlobales.contenedor, estilosGlobales.inlineBlock, e.cont)}>
|
||||||
<h1 style={{
|
<h1 style={{
|
||||||
"text-align": "center",
|
"text-align": "center",
|
||||||
"font-size": "1.75rem",
|
"font-size": "1.75rem",
|
||||||
|
Loading…
Reference in New Issue
Block a user