making login page beautiful

This commit is contained in:
2025-09-01 15:06:03 +03:00
parent 96cc825985
commit a918b831a7
5 changed files with 220 additions and 38 deletions

View File

@ -0,0 +1,54 @@
.form-field {
position: relative;
margin-bottom: 14px;
width: 100%;
}
.form-field input {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-field input:focus {
outline: none;
border-color: #4a90e2;
}
.form-field input.error {
border-color: #e74c3c;
}
.error-message {
position: absolute;
bottom: -20px;
left: 0;
color: #e74c3c;
font-size: 12px;
opacity: 0;
transform: translateY(10px);
animation: slideUp 0.3s ease forwards;
}
.field-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
color: #a0aec0;
}
.form-field:has(.field-icon) input {
padding-left: 40px;
}
@keyframes slideUp {
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -0,0 +1,99 @@
import React, { useState } from 'react';
import './FormField.css';
interface FormFieldProperties {
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
placeholder?: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
required?: boolean;
errorMessage?: string;
showError?: boolean;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
disabled?: boolean;
autoComplete?: string;
customValidator?: (value: string) => string | null;
showErrorInitially?: boolean;
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}
const FormField: React.FC<FormFieldProperties> = ({
type = 'text',
placeholder,
value,
onChange,
required = false,
errorMessage = 'Please fill out this field',
showError = false,
onBlur,
onFocus,
className = '',
disabled = false,
autoComplete = 'off',
customValidator,
showErrorInitially = false,
icon: Icon,
}) => {
const [touched, setTouched] = useState<boolean>(showErrorInitially);
const [customError, setCustomError] = useState<string>('');
const validateField = (value: string): string => {
if (required && !value) {
return errorMessage;
}
if (customValidator) {
const customValidation = customValidator(value);
if (customValidation) {
return customValidation;
}
}
return '';
};
const shouldShowError = showError || (touched && validateField(value));
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
setTouched(true);
const error = validateField(value);
setCustomError(error);
onBlur?.(e);
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>): void => {
setCustomError('');
onFocus?.(e);
};
return (
<div className={`form-field ${className}`}>
{Icon && (
<div className="field-icon">
<Icon />
</div>
)}
<input
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={handleBlur}
onFocus={handleFocus}
required={required}
disabled={disabled}
autoComplete={autoComplete}
className={shouldShowError ? 'error' : ''}
/>
{shouldShowError && (
<div className="error-message">
{customError || errorMessage}
</div>
)}
</div>
);
};
export default FormField;

View File

@ -20,9 +20,5 @@
}
body {
margin: 0;
display: grid;
place-items: center;
min-width: 320px;
min-height: 100vh;
display: flex;
}

16
src/pages/login/login.css Normal file
View File

@ -0,0 +1,16 @@
.login-div {
display: flex;
height: 100%;
justify-content: flex-end;
align-items: center;
border: 1px solid green;
}
.login-right-div {
display: flex;
height: 100%;
justify-content: flex-end;
align-items: center;
flex-direction: column;
border: 1px solid blue;
}

View File

@ -5,6 +5,9 @@ import { toast } from "react-toastify"
import useAuth from "../../auth/auth";
import FormField from "../../components/FormField"
import "./login.css"
const Login = () => {
const { setToken } = useAuth();
@ -13,9 +16,20 @@ const Login = () => {
const navigate = useNavigate();
const location = useLocation();
const [usernameEmpty, setUsernameEmpty] = useState(false);
const [passwordEmpty, setPasswordEmpty] = useState(false);
const from = location.state?.from?.pathname || "/";
const handleSubmit = () => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!username || !password) {
if (!username) setUsernameEmpty(true);
if (!password) setPasswordEmpty(true);
return;
}
// TODO: request to API
if ((username === "admin" && password === "1234") || (username === "a" && password === "a")) {
const token = "todo.jwt.token";
@ -27,46 +41,49 @@ const Login = () => {
};
return (
<div style={{
display: "flex",
justifyContent: "right",
alignItems: "center",
minHeight: "100vh",
flexDirection: "column",
gap: "15px",
margin: "0 15px"
}}>
<h2>Login</h2>
<form onSubmit={(e) => {e.preventDefault(); handleSubmit()}}
id="form-id"
<div className="login-right-div">
<h2
style={{
display: "flex"
}}
>
Login
</h2>
<form onSubmit={handleSubmit}
style={{
flexDirection: "column",
display: "flex",
gap: "5px",
width: "100%",
maxWidth: "400px",
gap: "10px",
alignItems: "center",
}}
noValidate
>
<div>
<input
<FormField
type="text"
placeholder=" Username"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
showError={usernameEmpty}
errorMessage="Please enter your username"
className="test1"
/>
</div>
<div>
<input
<FormField
type="password"
placeholder=" Password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
showError={passwordEmpty}
errorMessage="Please enter your password"
className="test2"
/>
</div>
<button type="submit" style={{ marginTop: "10px" }}>
<button type="submit"
style={{
display: "flex",
marginTop: "10px"
}}
>
Login
</button>
</form>