diff --git a/package.json b/package.json index bc50d1e..e36a6c2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@tanstack/react-query": "^5.53.3", "apexcharts": "^3.52.0", "autoprefixer": "^10.4.20", @@ -17,12 +18,15 @@ "react": "^18.3.1", "react-apexcharts": "^1.4.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", "react-router-dom": "^6.26.1", "react-select": "^5.8.0", + "react-toastify": "^10.0.5", "styled-components": "^6.1.13", "tailwindcss": "^3.4.10", "tsconfig-paths": "^4.2.0", - "vite-tsconfig-paths": "^5.0.0" + "vite-tsconfig-paths": "^5.0.0", + "yup": "^1.4.0" }, "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/src/App.tsx b/src/App.tsx index ceb115a..c3023aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,9 @@ +import { ToastContainer } from 'react-toastify' import { Router } from './router' export const App = () => ( - + <> + + + ) diff --git a/src/components/button/index.tsx b/src/components/button/index.tsx index 6cc68ba..9c39830 100644 --- a/src/components/button/index.tsx +++ b/src/components/button/index.tsx @@ -3,11 +3,12 @@ import { ReactNode, HTMLAttributes, HTMLProps, createElement } from 'react' export type ButtonProps = HTMLAttributes & HTMLProps & { children?: ReactNode, type?: "button" | "submit" | "reset", + color?: string, } -export const Button = ({ children, className, ...props }: ButtonProps) => { +export const Button = ({ children, className, color, ...props }: ButtonProps) => { let tag = 'button' - let localClassName = 'text-white bg-primary hover:bg-dark font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 transition-all duration-300' + let localClassName = 'text-white disabled:bg-dark hover:bg-dark font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 transition-all duration-300' if (props.href) { tag = 'a' @@ -17,5 +18,7 @@ export const Button = ({ children, className, ...props }: ButtonProps) => { localClassName = `${localClassName} ${className}` } + localClassName = `${localClassName} ${color ? color : `bg-primary`}` + return createElement(tag, { ...props, className: localClassName }, children) } diff --git a/src/components/form/input/index.tsx b/src/components/form/input/index.tsx index 537d6cb..88feac4 100644 --- a/src/components/form/input/index.tsx +++ b/src/components/form/input/index.tsx @@ -1,46 +1,2 @@ -import { ReactNode, useEffect, useState } from 'react' -import { InputField, InputFieldProps } from './input-field' -import { Typography } from '../../Typography' -import { Adapters } from './adapter' -import { Label } from './label' - -export type Option = { - value: any - label: string -} - -export type InputProps = InputFieldProps & { - name: string - label?: string - helpText?: ReactNode - options?: Option[] - rows?: number - isMulti?: boolean -} - -export const Input = ({ label, helpText, type = 'text', options, onChange, ...props }: InputProps) => { - const [hasAdapter, setHasAdapter] = useState(false) - - useEffect(() => { - setHasAdapter(!!Adapters[type]) - }, [type]) - - let localHelpTextClassName = 'mt-2 !mb-0 text-sm text-gray-500 dark:text-gray-400' - - if (props?.error) { - localHelpTextClassName += ' !text-red-500' - } - - return ( -
- {label &&
- ) -} +export * from './input-controller' +export * from './input' \ No newline at end of file diff --git a/src/components/form/input/input-controller.tsx b/src/components/form/input/input-controller.tsx new file mode 100644 index 0000000..974f630 --- /dev/null +++ b/src/components/form/input/input-controller.tsx @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { + Control, Controller, FieldErrors, +} from 'react-hook-form' +import { Input, InputProps } from './input' + +type InputControllerProps = { + control: Control + name: keyof FieldErrors + errors: FieldErrors + size?: number +} & InputProps + +export const InputController = ({ + name, control, errors, size, ...props +}: InputControllerProps) => ( + ( + {errors?.[name]?.message}} + onChange={onChange} + width={size ? '100%' : props.width} + {...props} + /> + )} + /> +) + diff --git a/src/components/form/input/input.tsx b/src/components/form/input/input.tsx new file mode 100644 index 0000000..537d6cb --- /dev/null +++ b/src/components/form/input/input.tsx @@ -0,0 +1,46 @@ +import { ReactNode, useEffect, useState } from 'react' +import { InputField, InputFieldProps } from './input-field' +import { Typography } from '../../Typography' +import { Adapters } from './adapter' +import { Label } from './label' + +export type Option = { + value: any + label: string +} + +export type InputProps = InputFieldProps & { + name: string + label?: string + helpText?: ReactNode + options?: Option[] + rows?: number + isMulti?: boolean +} + +export const Input = ({ label, helpText, type = 'text', options, onChange, ...props }: InputProps) => { + const [hasAdapter, setHasAdapter] = useState(false) + + useEffect(() => { + setHasAdapter(!!Adapters[type]) + }, [type]) + + let localHelpTextClassName = 'mt-2 !mb-0 text-sm text-gray-500 dark:text-gray-400' + + if (props?.error) { + localHelpTextClassName += ' !text-red-500' + } + + return ( +
+ {label &&
+ ) +} diff --git a/src/components/toast/index.tsx b/src/components/toast/index.tsx new file mode 100644 index 0000000..ce86490 --- /dev/null +++ b/src/components/toast/index.tsx @@ -0,0 +1 @@ +export * from 'react-toastify' \ No newline at end of file diff --git a/src/config/routes.tsx b/src/config/routes.tsx index e14c0e1..76588ab 100644 --- a/src/config/routes.tsx +++ b/src/config/routes.tsx @@ -5,6 +5,7 @@ import { ReactNode } from 'react' import { NoTemplate } from '@pages/no-template' import { Forms } from '@pages/forms' import { LoginLayout } from '@layouts/default/login.layout' +import { LoginPage } from '@pages/login' type RouteConfigItemProps = { title?: ReactNode, @@ -49,7 +50,7 @@ export const RoutesConfig: RouteConfigItem[] = [ menuLabel: 'Login', menuIcon: 'passkey', }, - element: <>, + element: , }, { path: '/empty', diff --git a/src/layouts/default/login.layout.tsx b/src/layouts/default/login.layout.tsx index b98b7cf..7166804 100644 --- a/src/layouts/default/login.layout.tsx +++ b/src/layouts/default/login.layout.tsx @@ -1,28 +1,19 @@ -import { FormEvent, useCallback } from 'react' +import { useCallback } from 'react' import { useDarkMode } from '../../contexts/dark-mode' import { Typography } from '../../components/Typography' import { Toggle } from '../../components/form/toggle' import { Rounded } from '../../components/rounded' import { Img } from '../../components/img' -import { Card, CardBackground } from '../../components/card' -import { FormContainer } from '../../components/form/form-container' -import { Input } from '../../components/form/input' -import { Button } from '../../components/button' -import { useNavigate } from 'react-router-dom' +import { CardBackground } from '../../components/card' +import { Outlet } from 'react-router-dom' export const LoginLayout = () => { const { isDarkMode, toggleDarkMode } = useDarkMode() - const navigate = useNavigate() const darkModeHandler = useCallback(() => { toggleDarkMode() }, [isDarkMode]) - const submit = (e: FormEvent) => { - e.preventDefault() - navigate('/') - } - return (
@@ -37,40 +28,7 @@ export const LoginLayout = () => {
-
- - - - - Sign in to your account - - - - - - - - - - - -
+
diff --git a/src/layouts/default/style.css b/src/layouts/default/style.css index aeffff1..995bfd9 100644 --- a/src/layouts/default/style.css +++ b/src/layouts/default/style.css @@ -1,5 +1,6 @@ @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&family=Space+Grotesk:wght@300..700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0'); +@import 'react-toastify/dist/ReactToastify.css'; @tailwind base; @tailwind components; diff --git a/src/pages/forms.tsx b/src/pages/forms.tsx index 5229511..46b113a 100644 --- a/src/pages/forms.tsx +++ b/src/pages/forms.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import { Button } from '../components/button' import { Card } from '../components/card' import { FormContainer } from '../components/form/form-container' @@ -17,6 +17,17 @@ export const Forms = () => { const { handleOnChange, handleOnChangeMultiple } = useOnChange(formData, setFormData) + const reset = useCallback(() => { + setFormData({ + 'input-password': null, + 'input-text-checkbox': null, + 'input-text-radio': null, + 'input-text-toggle': null, + 'input-text-select': null, + 'input-text-select-multiple': null, + }) + }, [formData]) + return ( @@ -154,7 +165,7 @@ export const Forms = () => {
- +
diff --git a/src/pages/login.tsx b/src/pages/login.tsx new file mode 100644 index 0000000..70f94ca --- /dev/null +++ b/src/pages/login.tsx @@ -0,0 +1,90 @@ +import { useNavigate } from 'react-router-dom' +import { SubmitHandler, useForm } from "react-hook-form" +import { yupResolver } from "@hookform/resolvers/yup" +import * as yup from "yup" +import { Button } from '../components/button' +import { Card } from '../components/card' +import { FormContainer } from '../components/form/form-container' +import { InputController } from '../components/form/input' +import { Img } from '../components/img' +import { Rounded } from '../components/rounded' +import { Typography } from '../components/Typography' +import { toast } from '../components/toast' +import { useState } from 'react' + +type Form = { + email: string + password: string + remember?: boolean +} + +const Schema = yup.object().shape({ + email: yup.string().email().required(), + password: yup.string().min(6).required(), + remember: yup.boolean().optional(), +}) + +export const LoginPage = () => { + const [disabled, setDisabled] = useState(false) + const navigate = useNavigate() + + const { + control, + handleSubmit, + formState: { errors }, + } = useForm
({ + resolver: yupResolver(Schema), + }) + + const submit: SubmitHandler = (data) => { + toast.success(`Sign in with ${data.email}!`) + toast.success(`Welcome!`) + + setDisabled(true) + + setTimeout(() => navigate('/'), 1000) + } + + return ( +
+ + + + + Sign in to your account + + + + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7c7ff64..680c8fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -479,6 +479,11 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.7.tgz#d0ece53ce99ab5a8e37ebdfe5e32452a2bfc073e" integrity sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA== +"@hookform/resolvers@^3.9.0": + version "3.9.0" + resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.9.0.tgz#cf540ac21c6c0cd24a40cf53d8e6d64391fb753d" + integrity sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg== + "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" @@ -1040,6 +1045,11 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +clsx@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1991,6 +2001,11 @@ prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +property-expr@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" + integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -2016,6 +2031,11 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" +react-hook-form@^7.53.0: + version "7.53.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.0.tgz#3cf70951bf41fa95207b34486203ebefbd3a05ab" + integrity sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -2056,6 +2076,13 @@ react-select@^5.8.0: react-transition-group "^4.3.0" use-isomorphic-layout-effect "^1.1.2" +react-toastify@^10.0.5: + version "10.0.5" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-10.0.5.tgz#6b8f8386060c5c856239f3036d1e76874ce3bd1e" + integrity sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw== + dependencies: + clsx "^2.1.0" + react-transition-group@^4.3.0: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" @@ -2400,6 +2427,11 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -2412,6 +2444,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== + ts-api-utils@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" @@ -2448,6 +2485,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + typescript-eslint@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.0.1.tgz#e812ce16e9332c6c81cfa2f17aecf99b74473da7" @@ -2561,3 +2603,13 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yup@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.4.0.tgz#898dcd660f9fb97c41f181839d3d65c3ee15a43e" + integrity sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg== + dependencies: + property-expr "^2.0.5" + tiny-case "^1.0.3" + toposort "^2.0.2" + type-fest "^2.19.0"