diff --git a/src/components/FormField.css b/src/components/FormField.css new file mode 100644 index 0000000..252460b --- /dev/null +++ b/src/components/FormField.css @@ -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); + } +} diff --git a/src/components/FormField.tsx b/src/components/FormField.tsx new file mode 100644 index 0000000..1ca6fd6 --- /dev/null +++ b/src/components/FormField.tsx @@ -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) => void; + required?: boolean; + errorMessage?: string; + showError?: boolean; + onBlur?: (e: React.FocusEvent) => void; + onFocus?: (e: React.FocusEvent) => void; + className?: string; + disabled?: boolean; + autoComplete?: string; + customValidator?: (value: string) => string | null; + showErrorInitially?: boolean; + icon?: React.ComponentType>; +} + + +const FormField: React.FC = ({ + 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(showErrorInitially); + const [customError, setCustomError] = useState(''); + + 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): void => { + setTouched(true); + const error = validateField(value); + setCustomError(error); + onBlur?.(e); + }; + + const handleFocus = (e: React.FocusEvent): void => { + setCustomError(''); + onFocus?.(e); + }; + + return ( +
+ {Icon && ( +
+ +
+ )} + + {shouldShowError && ( +
+ {customError || errorMessage} +
+ )} +
+ ); +}; + +export default FormField; diff --git a/src/index.css b/src/index.css index 5e9a08f..de709c2 100644 --- a/src/index.css +++ b/src/index.css @@ -20,9 +20,5 @@ } body { - margin: 0; - display: grid; - place-items: center; - min-width: 320px; - min-height: 100vh; + display: flex; } diff --git a/src/pages/login/login.css b/src/pages/login/login.css new file mode 100644 index 0000000..036d87c --- /dev/null +++ b/src/pages/login/login.css @@ -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; +} diff --git a/src/pages/login/login.tsx b/src/pages/login/login.tsx index 78e8869..0e32568 100644 --- a/src/pages/login/login.tsx +++ b/src/pages/login/login.tsx @@ -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 ( -
-

Login

-
{e.preventDefault(); handleSubmit()}} - id="form-id" +
+

+ Login +

+ -
- setUsername(e.target.value)} - required + setUsername(e.target.value)} + required + showError={usernameEmpty} + errorMessage="Please enter your username" + className="test1" /> -
-
- setPassword(e.target.value)} - required + setPassword(e.target.value)} + required + showError={passwordEmpty} + errorMessage="Please enter your password" + className="test2" /> -
-