Main project structure #3
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 {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
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 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
|
||||
type="text"
|
||||
placeholder=" Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
<FormField
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
showError={usernameEmpty}
|
||||
errorMessage="Please enter your username"
|
||||
className="test1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
placeholder=" Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
<FormField
|
||||
type="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>
|
||||
|
||||
Reference in New Issue
Block a user