diff options
| author | Arne Rief <riearn@proton.me> | 2025-12-19 20:03:03 +0100 |
|---|---|---|
| committer | Arne Rief <riearn@proton.me> | 2025-12-19 20:03:03 +0100 |
| commit | 655ec610fcce8dd7748f10772d520bdff4f7c78e (patch) | |
| tree | 35b79f30d2cb5aea88cf76ce27f480da93cefd32 /frontend/src/pages/Login.tsx | |
Basic setup & login
Diffstat (limited to 'frontend/src/pages/Login.tsx')
| -rw-r--r-- | frontend/src/pages/Login.tsx | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..2e99a2f --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,148 @@ +import { useEffect, useState, type ChangeEvent, type FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; +import { FadeLoader } from "react-spinners"; +import Logo from "../components/Logo"; +import API_URL from "../config"; +import "../styles/Button.css"; +import "../styles/Login.css"; +import type { + ErrorResponse, + LoginFormData, + LoginResponse, +} from "../types/login"; + +const EMPTY_FORM_DATA: LoginFormData = { + email: "", + password: "", +}; + +function Login() { + const navigate = useNavigate(); + + const [errorMessage, setErrorMessage] = useState<string>(""); + const [formData, setFormData] = useState<LoginFormData>(EMPTY_FORM_DATA); + const [isLoading, setIsLoading] = useState<boolean>(false); + + function handleUserInput(event: ChangeEvent<HTMLInputElement>) { + if (errorMessage) { + setErrorMessage(""); + } + + setFormData((oldFormData) => ({ + ...oldFormData, + [event.target.name]: event.target.value, + })); + } + + async function handleLogin(event: FormEvent<HTMLFormElement>) { + event.preventDefault(); + setErrorMessage(""); + + if (!formData.email || !formData.password) { + setErrorMessage("E-mail address and password are required."); + return; + } + + setIsLoading(true); + + try { + const response = await fetch(`${API_URL}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + + if (!response.ok) { + const errorData: ErrorResponse = await response.json(); + throw new Error( + errorData.message || + `Login failed, status: ${response.status}` + ); + } + + const data: LoginResponse = await response.json(); + + localStorage.setItem("token-robot-tracker", data.token); + localStorage.setItem("user", JSON.stringify(data.user)); + setFormData(EMPTY_FORM_DATA); + + navigate("/dashboard", { replace: true }); + } catch (error) { + console.error(error); + + if (error instanceof Error) { + setErrorMessage(error.message); + } else { + setErrorMessage("An unexpected error occurred."); + } + } finally { + setIsLoading(false); + } + } + + // Clear local storage on component mounting + useEffect(() => { + localStorage.removeItem("token-robot-tracker"); + localStorage.removeItem("user"); + }, []); + + return ( + <div className="login-page"> + <div className="login-card"> + <Logo /> + <p className="subtitle">🤖 Please log in to use the app 🤖</p> + + {/* "noValidate" to enable manual errorMessage in handleLogin; alternatively omit "required" */} + <form className="login-form" onSubmit={handleLogin} noValidate> + <div className="form-group"> + <label htmlFor="email">E-Mail</label> + <input + type="email" + id="email" + name="email" + value={formData?.email || ""} + onChange={handleUserInput} + required + placeholder="Your e-mail..." + disabled={isLoading} + /> + </div> + + <div className="form-group"> + <label htmlFor="password">Password</label> + <input + type="password" + id="password" + name="password" + value={formData?.password || ""} + onChange={handleUserInput} + required + placeholder="Your password..." + disabled={isLoading} + /> + </div> + + {errorMessage && ( + <div className="error-message">{errorMessage}</div> + )} + + <button + className="btn btn-start" + disabled={isLoading} + type="submit" + > + {isLoading ? ( + <div className="loading-spinner-container"> + <FadeLoader /> + </div> + ) : ( + "Login" + )} + </button> + </form> + </div> + </div> + ); +} + +export default Login; |
