From c196ffc265f7df296b912e983196e6b14eb4a0c3 Mon Sep 17 00:00:00 2001 From: code002lover Date: Mon, 12 Jan 2026 18:32:59 +0100 Subject: [PATCH] improve login page --- .gitignore | 4 +- frontend/src/App.tsx | 18 +- frontend/src/Login.tsx | 423 +++++++++++++++++++++++++++++++++-------- 3 files changed, 356 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index b439660..e4ef5ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target/ -cache/ \ No newline at end of file +cache/ +tokens.bin +.auth_key \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e7530fc..eec7936 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -41,6 +41,15 @@ function App() { _setTheme(ev.data as string); }; + const addToast = useCallback((message: string, type: ToastType = "info") => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, message, type }]); + }, []); + + const removeToast = (id: number) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }; + useEffect(() => { if (theme !== "default" && theme !== "sakura") { document.body.classList.remove("sakura-theme"); @@ -73,15 +82,6 @@ function App() { return () => window.removeEventListener("unauthorized", handleUnauthorized); }, [addToast]); - const addToast = useCallback((message: string, type: ToastType = "info") => { - const id = Date.now(); - setToasts((prev) => [...prev, { id, message, type }]); - }, []); - - const removeToast = (id: number) => { - setToasts((prev) => prev.filter((t) => t.id !== id)); - }; - const fetchPeople = () => { if (!token) return; setIsLoading(true); diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx index f09499c..906c146 100644 --- a/frontend/src/Login.tsx +++ b/frontend/src/Login.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { LoginRequest, LoginResponse } from "../items"; interface LoginProps { @@ -6,11 +6,18 @@ interface LoginProps { } export function Login({ onLogin }: LoginProps) { + const usernameInputRef = useRef(null); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [fieldErrors, setFieldErrors] = useState<{ username?: string, password?: string }>({}); + const [isSuccess, setIsSuccess] = useState(false); + const [shakeCard, setShakeCard] = useState(false); + + useEffect(() => { + usernameInputRef.current?.focus(); + }, []); const validateForm = () => { const errors: { username?: string, password?: string } = {}; @@ -24,7 +31,11 @@ export function Login({ onLogin }: LoginProps) { e.preventDefault(); setError(""); - if (!validateForm()) return; + if (!validateForm()) { + setShakeCard(true); + setTimeout(() => setShakeCard(false), 500); + return; + } setIsSubmitting(true); @@ -44,110 +55,364 @@ export function Login({ onLogin }: LoginProps) { const response = LoginResponse.decode(new Uint8Array(buffer)); if (response.success) { + setIsSuccess(true); onLogin(response.token); } else { setError(response.message || "Login failed"); + setShakeCard(true); + setTimeout(() => setShakeCard(false), 500); } } catch (err) { console.error("Login error:", err); setError("An unexpected error occurred. Please try again."); + setShakeCard(true); + setTimeout(() => setShakeCard(false), 500); } finally { setIsSubmitting(false); } }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSubmit(e); + } + }; + return ( -
-

🎮 Login

-
-
- - { - setUsername(e.target.value); - if (fieldErrors.username) setFieldErrors({ ...fieldErrors, username: undefined }); - }} - placeholder="Enter your username" - style={{ - borderColor: fieldErrors.username ? "#f44336" : undefined, - borderWidth: fieldErrors.username ? "2px" : undefined - }} - /> - {fieldErrors.username && ( - - {fieldErrors.username} - - )} -
-
- - { - setPassword(e.target.value); - if (fieldErrors.password) setFieldErrors({ ...fieldErrors, password: undefined }); - }} - placeholder="Enter your password" - style={{ - borderColor: fieldErrors.password ? "#f44336" : undefined, - borderWidth: fieldErrors.password ? "2px" : undefined - }} - /> - {fieldErrors.password && ( - - {fieldErrors.password} - - )} -
- -
- {error && ( +
+ {isSubmitting && (
+
+
+ )} + + {isSuccess && ( +
+
+ ✓ +
+ + Success! + +
+ )} + +

+ 🎮 Login +

+

+ Welcome back! Please sign in to continue. +

+ +
+
+ +
+ + 👤 + + { + setUsername(e.target.value); + if (fieldErrors.username) setFieldErrors({ ...fieldErrors, username: undefined }); + }} + onKeyDown={handleKeyDown} + placeholder="Enter your username" + aria-label="Username" + aria-invalid={!!fieldErrors.username} + aria-describedby={fieldErrors.username ? "username-error" : undefined} + style={{ + width: "100%", + paddingLeft: "2.75rem", + paddingRight: "0.75rem", + paddingTop: "0.75rem", + paddingBottom: "0.75rem", + borderColor: fieldErrors.username ? "#f44336" : undefined, + borderWidth: fieldErrors.username ? "2px" : "1px", + fontSize: "1rem" + }} + /> +
+ {fieldErrors.username && ( + + ⚠️ {fieldErrors.username} + + )} +
+ +
+ +
+ + 🔒 + + { + setPassword(e.target.value); + if (fieldErrors.password) setFieldErrors({ ...fieldErrors, password: undefined }); + }} + onKeyDown={handleKeyDown} + placeholder="Enter your password" + aria-label="Password" + aria-invalid={!!fieldErrors.password} + aria-describedby={fieldErrors.password ? "password-error" : undefined} + style={{ + width: "100%", + paddingLeft: "2.75rem", + paddingRight: "0.75rem", + paddingTop: "0.75rem", + paddingBottom: "0.75rem", + borderColor: fieldErrors.password ? "#f44336" : undefined, + borderWidth: fieldErrors.password ? "2px" : "1px", + fontSize: "1rem" + }} + /> +
+ {fieldErrors.password && ( + + ⚠️ {fieldErrors.password} + + )} +
+ + +
+ + {error && ( +
⚠️ {error}
)} +
);