style login page

This commit is contained in:
Anika Raemer 2025-10-12 08:36:41 +02:00
parent 575eecfc69
commit 97b0e5bdd3
6 changed files with 128 additions and 60 deletions

View file

@ -11,6 +11,8 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"lucide": "^0.545.0",
"lucide-react": "^0.545.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.8.2" "react-router-dom": "^7.8.2"
@ -3371,6 +3373,21 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/lucide": {
"version": "0.545.0",
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.545.0.tgz",
"integrity": "sha512-mrBH0upkb1TH8ZLkf0XERQAKMdTJ1C+Offr7eSvanmdQ7JrV8jkrFw5jgRAS8fygiZgG4X9mfEg/BXCL+mNMRw==",
"license": "ISC"
},
"node_modules/lucide-react": {
"version": "0.545.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz",
"integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.18", "version": "0.30.18",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",

View file

@ -13,6 +13,8 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"lucide": "^0.545.0",
"lucide-react": "^0.545.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.8.2" "react-router-dom": "^7.8.2"

View file

@ -4,20 +4,15 @@
/* Custom recipe app styles */ /* Custom recipe app styles */
@layer components{ @layer components{
.app-container {
@apply p-4 flex; /* background */
.app-bg {
@apply flex items-center w-screen justify-center min-h-screen bg-gray-50
} }
/* headings */
.content-title{ .content-title{
@apply text-3xl font-black mb-8 text-blue-400; @apply text-3xl font-black mb-8 text-blue-900;
}
.main-view {
@apply w-2/3 pl-4;
}
.recipe-image {
@apply my-4 w-64;
} }
.section-heading { .section-heading {
@ -28,10 +23,16 @@
@apply font-semibold mb-2 mt-4; @apply font-semibold mb-2 mt-4;
} }
/* labels */
.label { .label {
@apply text-gray-600 @apply text-gray-600
} }
/* errors */
.error-text {
@apply text-sm text-red-600
}
/* buttons */ /* buttons */
.basic-button{ .basic-button{
@apply px-4 py-2 shadow-md rounded-lg whitespace-nowrap @apply px-4 py-2 shadow-md rounded-lg whitespace-nowrap

View file

@ -1,55 +1,64 @@
import { useState } from "react"; import { useState } from "react";
import Button from "./basics/Button"; import Button, { ButtonType } from "./basics/Button";
import type { LoginRequestDto } from "../api/dtos/LoginRequestDto"; import type { LoginRequestDto } from "../api/dtos/LoginRequestDto";
import type { LoginResponseDto } from "../api/dtos/LoginResponseDto"; import type { LoginResponseDto } from "../api/dtos/LoginResponseDto";
import { login } from "../api/points/AuthPoint"; import { login } from "../api/points/AuthPoint";
import { getRecipeListUrl } from "../routes"; import { getRecipeListUrl } from "../routes";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import PasswordField from "./basics/PasswordField";
export default function LoginPage(){ export default function LoginPage() {
const [userName, setUserName] = useState<string>(""); const [userName, setUserName] = useState<string>("");
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const executeLogin = async () =>{
const dto : LoginRequestDto = { const executeLogin = async () => {
userName: userName, const dto: LoginRequestDto = {
password: password userName,
} password,
try{ };
// @todo move to auth handler
console.log("trying to log in with " + dto.userName) try {
const loginResponse : LoginResponseDto = await login(dto); console.log("Trying to log in with " + dto.userName);
const loginResponse: LoginResponseDto = await login(dto);
localStorage.setItem("session", JSON.stringify(loginResponse)); localStorage.setItem("session", JSON.stringify(loginResponse));
console.log("Successfully logged on with user " + loginResponse.userData?.userName) console.log("Successfully logged in as " + loginResponse.userData?.userName);
setErrorMessage(null);
navigate(getRecipeListUrl()); navigate(getRecipeListUrl());
} catch(err){ } catch (err: any) {
// @todo show error in GUI console.error("Login failed:", err);
console.error(err); setErrorMessage("Login fehlgeschlagen! Bitte überprüfe Benutzername und Passwort.");
} }
} };
return(
<div className="p-6 max-w-2xl mx-auto"> return (
<div className="app-bg">
<div className="flex flex-col gap-3 max-w-sm w-full mx-auto p-6 bg-white rounded-2xl shadow-md">
<h2 className="content-title text-center mb-2">Anmeldung</h2>
<input <input
className="input-field" className="input-field"
placeholder="Benutzername" placeholder="Benutzername"
value = {userName} value={userName}
onChange={e => { onChange={(e) => setUserName(e.target.value)}
setUserName(e.target.value)
}}
/> />
{/* @todo Password mode!!! */}
<input <PasswordField
className="input-field" onPasswordChanged = {setPassword}
placeholder="Passwort"
value = {password}
onChange={e => {
setPassword(e.target.value)
}}
/> />
{errorMessage && (
<p className="error-text text-center">{errorMessage}</p>
)}
<Button <Button
buttonType={ButtonType.PrimaryButton}
text="Login" text="Login"
onClick= {executeLogin} onClick={executeLogin}
/> />
</div> </div>
</div>
); );
} }

View file

@ -0,0 +1,39 @@
import { Eye, EyeOff } from "lucide-react";
import { useState } from "react";
type PasswordFieldProps = {
onPasswordChanged: (password : string) => void
}
/**
* Password field component
*/
export default function PasswordField({onPasswordChanged} : PasswordFieldProps){
const [showPassword, setShowPassword] = useState(false);
const [password, setPassword] = useState<string>("");
const iconSize = 20;
const changePassword = (password : string) => {
setPassword(password);
onPasswordChanged(password)
}
return (
<div className="relative">
<input
className="input-field pr-10"
type={showPassword ? "text" : "password"}
placeholder="Passwort"
value={password}
onChange={(e) => changePassword(e.target.value)}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
aria-label={showPassword ? "Passwort ausblenden" : "Passwort anzeigen"}
>
{showPassword ? <EyeOff size={iconSize} /> : <Eye size={iconSize} />}
</button>
</div>
);
}

View file

@ -39,12 +39,12 @@ export default function RecipeListPage() {
if(!recipeList) { return <div>Loading!</div>} if(!recipeList) { return <div>Loading!</div>}
return ( return (
/*Container spanning entire screen used to center content horizontally */ /*Container spanning entire screen used to center content horizontally */
<div className="w-screen min-h-screen flex justify-center"> <div className="app-bg">
{/* Container defining the maximum width of the content */} {/* Container defining the maximum width of the content */}
<div className="bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8"> <div className="bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8">
{/* Header - remains in position when scrolling */} {/* Header - remains in position when scrolling */}
<div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300"> <div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
<h1 className="content-title text-blue-900">Recipes</h1> <h1 className="content-title">Recipes</h1>
<RecipeListToolbar <RecipeListToolbar
onAddClicked={handleAdd} onAddClicked={handleAdd}
onSearchStringChanged={setSearchString} onSearchStringChanged={setSearchString}