diff --git a/frontend/svalyn-studio-app/src/forms/useForm.tsx b/frontend/svalyn-studio-app/src/forms/useForm.tsx new file mode 100644 index 0000000..82214ca --- /dev/null +++ b/frontend/svalyn-studio-app/src/forms/useForm.tsx @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { TextFieldProps } from '@mui/material/TextField'; +import { useState } from 'react'; +import { UseFormProps, UseFormState, UseFormValue, ValidationRule } from './useForm.types'; + +export function useForm({ + initialValue, + validationRules, +}: UseFormProps): UseFormValue { + const [state, setState] = useState>({ + data: initialValue, + isFormValid: false, + }); + + const getTextFieldProps = (name: keyof FormDataType): TextFieldProps => { + return { + value: state.data[name], + onChange: (event) => { + const { + target: { value }, + } = event; + setState((prevState) => { + const newData: FormDataType = { ...prevState.data, [name]: value }; + const isFormValid = validate(newData, validationRules); + return { ...prevState, data: newData, isFormValid }; + }); + }, + }; + }; + + return { + data: state.data, + isFormValid: state.isFormValid, + getTextFieldProps, + }; +} + +function validate( + data: FormDataType, + validationRules: { [FormDataProperty in keyof FormDataType]: ValidationRule } +): boolean { + let isFormValid: boolean = true; + for (const property in data) { + const validationRule = validationRules[property]; + if (validationRule) { + const isValidProperty = validationRule(data, property); + isFormValid = isFormValid && isValidProperty; + } + } + return isFormValid; +} diff --git a/frontend/svalyn-studio-app/src/forms/useForm.types.ts b/frontend/svalyn-studio-app/src/forms/useForm.types.ts new file mode 100644 index 0000000..8c86ac3 --- /dev/null +++ b/frontend/svalyn-studio-app/src/forms/useForm.types.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { TextFieldProps } from '@mui/material/TextField'; + +export interface UseFormProps { + initialValue: FormDataType; + validationRules: { [FormDataProperty in keyof FormDataType]: ValidationRule }; +} + +export interface UseFormValue { + data: FormDataType; + isFormValid: boolean; + getTextFieldProps: (name: keyof FormDataType) => TextFieldProps; +} + +export type ValidationRule = ( + data: FormDataType, + property: FormDataProperty +) => boolean; + +export interface UseFormState { + data: FormDataType; + isFormValid: boolean; +} diff --git a/frontend/svalyn-studio-app/src/new/NewOrganizationView.tsx b/frontend/svalyn-studio-app/src/new/NewOrganizationView.tsx index 3830081..bc42b29 100644 --- a/frontend/svalyn-studio-app/src/new/NewOrganizationView.tsx +++ b/frontend/svalyn-studio-app/src/new/NewOrganizationView.tsx @@ -28,42 +28,28 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { Navigate, Link as RouterLink } from 'react-router-dom'; +import { useForm } from '../forms/useForm'; import { Navbar } from '../navbars/Navbar'; import { ErrorSnackbar } from '../snackbar/ErrorSnackbar'; -import { NewOrganizationViewState } from './NewOrganizationView.types'; +import { NewOrganizationViewFormData, NewOrganizationViewState } from './NewOrganizationView.types'; import { useCreateOrganization } from './useCreateOrganization'; import { CreateOrganizationInput } from './useCreateOrganization.types'; export const NewOrganizationView = () => { const [state, setState] = useState({ - name: '', - organizationId: '', - isFormValid: false, message: null, }); - const handleNameChanged: React.ChangeEventHandler = (event) => { - const { - target: { value }, - } = event; - - setState((prevState) => ({ - ...prevState, - name: value, - isFormValid: value.length > 0 && prevState.organizationId.length > 0, - })); - }; - - const handleOrganizationIdChanged: React.ChangeEventHandler = (event) => { - const { - target: { value }, - } = event; - setState((prevState) => ({ - ...prevState, - organizationId: value, - isFormValid: value.length > 0 && prevState.name.length > 0, - })); - }; + const { data, isFormValid, getTextFieldProps } = useForm({ + initialValue: { + name: '', + organizationId: '', + }, + validationRules: { + name: (data, property) => data[property].length > 0, + organizationId: (data, property) => data[property].length > 0, + }, + }); const [createOrganization, { organization, message }] = useCreateOrganization(); useEffect(() => { @@ -72,11 +58,13 @@ export const NewOrganizationView = () => { } }, [message]); - const handleCreateOrganization: React.MouseEventHandler = () => { + const handleCreateOrganization: React.FormEventHandler = (event) => { + event.preventDefault(); + const input: CreateOrganizationInput = { id: crypto.randomUUID(), - identifier: state.organizationId, - name: state.name, + identifier: data.organizationId, + name: data.name, }; createOrganization(input); }; @@ -95,45 +83,40 @@ export const NewOrganizationView = () => { theme.spacing(2) }}> - - - Let's create your organization - - - - - - Back to the homepage - - +
+ + + Let's create your organization + + + + + + Back to the homepage + + +
diff --git a/frontend/svalyn-studio-app/src/new/NewOrganizationView.types.ts b/frontend/svalyn-studio-app/src/new/NewOrganizationView.types.ts index d0b2206..0152f5d 100644 --- a/frontend/svalyn-studio-app/src/new/NewOrganizationView.types.ts +++ b/frontend/svalyn-studio-app/src/new/NewOrganizationView.types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Stéphane Bégaudeau. + * Copyright (c) 2022, 2023 Stéphane Bégaudeau. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and * associated documentation files (the "Software"), to deal in the Software without restriction, @@ -18,8 +18,10 @@ */ export interface NewOrganizationViewState { + message: string | null; +} + +export interface NewOrganizationViewFormData { name: string; organizationId: string; - isFormValid: boolean; - message: string | null; }