making login page beautiful
This commit is contained in:
54
src/components/FormField.css
Normal file
54
src/components/FormField.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/components/FormField.tsx
Normal file
99
src/components/FormField.tsx
Normal 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;
|
||||||
@ -20,9 +20,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
display: flex;
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/pages/login/login.css
Normal file
16
src/pages/login/login.css
Normal 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;
|
||||||
|
}
|
||||||
@ -5,6 +5,9 @@ import { toast } from "react-toastify"
|
|||||||
|
|
||||||
import useAuth from "../../auth/auth";
|
import useAuth from "../../auth/auth";
|
||||||
|
|
||||||
|
import FormField from "../../components/FormField"
|
||||||
|
import "./login.css"
|
||||||
|
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { setToken } = useAuth();
|
const { setToken } = useAuth();
|
||||||
@ -13,9 +16,20 @@ const Login = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const [usernameEmpty, setUsernameEmpty] = useState(false);
|
||||||
|
const [passwordEmpty, setPasswordEmpty] = useState(false);
|
||||||
|
|
||||||
const from = location.state?.from?.pathname || "/";
|
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
|
// TODO: request to API
|
||||||
if ((username === "admin" && password === "1234") || (username === "a" && password === "a")) {
|
if ((username === "admin" && password === "1234") || (username === "a" && password === "a")) {
|
||||||
const token = "todo.jwt.token";
|
const token = "todo.jwt.token";
|
||||||
@ -27,46 +41,49 @@ const Login = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="login-right-div">
|
||||||
display: "flex",
|
<h2
|
||||||
justifyContent: "right",
|
style={{
|
||||||
alignItems: "center",
|
display: "flex"
|
||||||
minHeight: "100vh",
|
}}
|
||||||
flexDirection: "column",
|
>
|
||||||
gap: "15px",
|
Login
|
||||||
margin: "0 15px"
|
</h2>
|
||||||
}}>
|
<form onSubmit={handleSubmit}
|
||||||
<h2>Login</h2>
|
|
||||||
<form onSubmit={(e) => {e.preventDefault(); handleSubmit()}}
|
|
||||||
id="form-id"
|
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "5px",
|
gap: "10px",
|
||||||
width: "100%",
|
|
||||||
maxWidth: "400px",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
|
noValidate
|
||||||
>
|
>
|
||||||
<div>
|
<FormField
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
|
showError={usernameEmpty}
|
||||||
|
errorMessage="Please enter your username"
|
||||||
|
className="test1"
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
|
showError={passwordEmpty}
|
||||||
|
errorMessage="Please enter your password"
|
||||||
|
className="test2"
|
||||||
/>
|
/>
|
||||||
</div>
|
<button type="submit"
|
||||||
<button type="submit" style={{ marginTop: "10px" }}>
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
marginTop: "10px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user