From b31afdbd12e5af2e645eb5b08430a929bd18441e Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Wed, 23 Apr 2025 17:25:17 +0200 Subject: [PATCH 1/5] feat: form exemple --- src/tests/MyForm.tsx | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/tests/MyForm.tsx diff --git a/src/tests/MyForm.tsx b/src/tests/MyForm.tsx new file mode 100644 index 0000000..0c90b1c --- /dev/null +++ b/src/tests/MyForm.tsx @@ -0,0 +1,55 @@ +import { InputLabel, Input, Button } from '@mui/material' +import { useState } from 'react' + +type FormValues = { + firstName: string + lastName: string +} + +export function MyForm() { + const [formValues, setFormValues] = useState({ + firstName: '', + lastName: '', + }) + + const handleChange: React.ComponentProps<'input'>['onChange'] = (e) => { + const { name, value } = e.target + + setFormValues({ + ...formValues, + [name]: value, + }) + } + + const handleSubmit = () => { + // ... + } + + return ( +
+ First Name + + Last Name + + +
+ ) +} From 7a2186251ae7a73eae6b9331f301a5a6c7ff852d Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Thu, 24 Apr 2025 11:05:55 +0200 Subject: [PATCH 2/5] feat: add create item form and handle submission --- src/components/rules/CreateItemForm.tsx | 84 ++++++++++++++++++++++++- src/modules/rules.ts | 22 +++++++ src/styles/rules/form.css | 18 ++++++ 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/styles/rules/form.css diff --git a/src/components/rules/CreateItemForm.tsx b/src/components/rules/CreateItemForm.tsx index 490457c..2697a8f 100644 --- a/src/components/rules/CreateItemForm.tsx +++ b/src/components/rules/CreateItemForm.tsx @@ -1,7 +1,85 @@ +import '@/styles/rules/form.css' +import { useState } from 'react' +import { useAppDispatch } from '@/store' +import { addItem } from '@/modules/rules' +import { InputLabel, Input, Button } from '@mui/material' +import AddIcon from '@mui/icons-material/Add' + +type FormValues = { + name: string + price: number + linesPerMillisecond: number +} + export function CreateItemForm() { + const dispatch = useAppDispatch() + + const [formValues, setFormValues] = useState({ + name: '', + price: 0, + linesPerMillisecond: 0, + }) + + const handleChange: React.ComponentProps<'input'>['onChange'] = (e) => { + const { name, value } = e.target + setFormValues({ + ...formValues, + [name]: value, + }) + } + + const handleSubmit: React.ComponentProps<'form'>['onSubmit'] = (e) => { + e.preventDefault() + dispatch(addItem(formValues)) + } + return ( -
- CREATE ITEM FORM -
+
+
+ Name * + +
+
+ Price * + +
+
+ Lines per millisecond * + +
+ +
) } diff --git a/src/modules/rules.ts b/src/modules/rules.ts index cee91da..b814f62 100644 --- a/src/modules/rules.ts +++ b/src/modules/rules.ts @@ -21,6 +21,24 @@ export const fetchItems = createAsyncThunk( }, ) +export const addItem = createAsyncThunk( + 'rules/addItem', + async (itemData: Omit, { dispatch }) => { + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/shop/items`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(itemData), + }) + + const newItem = await response.json() as Item + + dispatch(itemReceived(newItem)) + }, +) + const rules = createSlice({ name: 'rule', initialState: INITIAL_STATE, @@ -28,11 +46,15 @@ const rules = createSlice({ fetchedItems: (state, action: PayloadAction) => { state.items = action.payload }, + itemReceived: (state, action: PayloadAction) => { + state.items.push(action.payload) + }, }, }) const { fetchedItems, + itemReceived, } = rules.actions export { diff --git a/src/styles/rules/form.css b/src/styles/rules/form.css new file mode 100644 index 0000000..5239d78 --- /dev/null +++ b/src/styles/rules/form.css @@ -0,0 +1,18 @@ +.form { + display: flex; + flex-direction: column; + gap: 1rem; + + .input { + width: 100%; + padding: 0.5rem; + margin: 0.25rem 0; + border: 1px solid #ccc; + border-radius: 0.25rem; + box-sizing: border-box; + } + + button { + margin-inline: auto; + } +} From ed50761354be25372b646349edd7f69b17bb0fc4 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Thu, 24 Apr 2025 11:51:23 +0200 Subject: [PATCH 3/5] feat: add form validation and error handling in CreateItemForm and handle request states --- src/components/rules/CreateItemForm.tsx | 79 ++++++++++++++++++++++++- src/modules/rules.ts | 12 +++- src/styles/rules/form.css | 6 ++ src/types.ts | 9 +++ 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/components/rules/CreateItemForm.tsx b/src/components/rules/CreateItemForm.tsx index 2697a8f..bcad4f9 100644 --- a/src/components/rules/CreateItemForm.tsx +++ b/src/components/rules/CreateItemForm.tsx @@ -1,9 +1,12 @@ import '@/styles/rules/form.css' -import { useState } from 'react' -import { useAppDispatch } from '@/store' -import { addItem } from '@/modules/rules' +import { useEffect, useState } from 'react' +import { RootState, useAppDispatch } from '@/store' +import { addItem, setAddItemRequestStatus } from '@/modules/rules' import { InputLabel, Input, Button } from '@mui/material' import AddIcon from '@mui/icons-material/Add' +import { useSelector } from 'react-redux' +import { useNavigate } from 'react-router' +import { RequestStatus } from '@/types' type FormValues = { name: string @@ -11,8 +14,24 @@ type FormValues = { linesPerMillisecond: number } +type FormErrors = { + name?: string + price?: string + linesPerMillisecond?: string +} + export function CreateItemForm() { const dispatch = useAppDispatch() + const navigate = useNavigate() + const requestStatus = useSelector((state: RootState) => state.rules.addItemRequestStatus) + + useEffect(() => { + if (requestStatus === RequestStatus.Succeeded) { + dispatch(setAddItemRequestStatus(RequestStatus.Idle)) + + navigate('/rules') + } + }, [requestStatus, navigate, dispatch]) const [formValues, setFormValues] = useState({ name: '', @@ -20,6 +39,8 @@ export function CreateItemForm() { linesPerMillisecond: 0, }) + const [formErrors, setFormErrors] = useState({}) + const handleChange: React.ComponentProps<'input'>['onChange'] = (e) => { const { name, value } = e.target setFormValues({ @@ -28,8 +49,41 @@ export function CreateItemForm() { }) } + const validateForm = () => { + const errors: FormErrors = {} + + if (!formValues.name) { + errors.name = 'Name is required' + } + + if (!formValues.price) { + errors.price = 'Price is required' + } + + if (formValues.price < 0) { + errors.price = 'Price must be a positive number' + } + + if (!formValues.linesPerMillisecond) { + errors.linesPerMillisecond = 'Lines per millisecond is required' + } + + if (formValues.linesPerMillisecond <= 0) { + errors.linesPerMillisecond = 'Lines per millisecond must be greater than 0' + } + + setFormErrors(errors) + + return Object.keys(errors).length === 0 + } + const handleSubmit: React.ComponentProps<'form'>['onSubmit'] = (e) => { e.preventDefault() + + if (!validateForm()) { + return + } + dispatch(addItem(formValues)) } @@ -44,8 +98,14 @@ export function CreateItemForm() { name="name" placeholder="Item Name" value={formValues.name} + error={formErrors.name != null} onChange={handleChange} /> + {formErrors.name && ( +

+ {formErrors.name} +

+ )}
Price * @@ -55,8 +115,14 @@ export function CreateItemForm() { type="number" name="price" value={formValues.price} + error={formErrors.price != null} onChange={handleChange} /> + {formErrors.price && ( +

+ {formErrors.price} +

+ )}
Lines per millisecond * @@ -69,12 +135,19 @@ export function CreateItemForm() { step: 0.1, }} value={formValues.linesPerMillisecond} + error={formErrors.linesPerMillisecond != null} onChange={handleChange} /> + {formErrors.linesPerMillisecond && ( +

+ {formErrors.linesPerMillisecond} +

+ )}
+ ) } diff --git a/src/modules/rules.ts b/src/modules/rules.ts index 323d9cb..b0559b3 100644 --- a/src/modules/rules.ts +++ b/src/modules/rules.ts @@ -5,11 +5,13 @@ import { RequestStatus, type Item, type TRequestStatus } from '@/types' type RulesState = { items: Item[] addItemRequestStatus: TRequestStatus + editItemRequestStatus: TRequestStatus } const INITIAL_STATE: RulesState = { items: [], addItemRequestStatus: RequestStatus.Idle, + editItemRequestStatus: RequestStatus.Idle, } // Side Effects / thunks @@ -44,6 +46,26 @@ export const addItem = createAsyncThunk( }, ) +export const editItem = createAsyncThunk( + 'rules/editItem', + async (itemData: Item, { dispatch }) => { + dispatch(setEditItemRequestStatus(RequestStatus.Loading)) + + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/shop/items/${itemData.id}`, { + method: 'PUT', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(itemData), + }) + const updatedItem = await response.json() as Item + + dispatch(itemUpdated(updatedItem)) + dispatch(setEditItemRequestStatus(RequestStatus.Succeeded)) + }, +) + const rules = createSlice({ name: 'rule', initialState: INITIAL_STATE, @@ -54,21 +76,34 @@ const rules = createSlice({ itemReceived: (state, action: PayloadAction) => { state.items.push(action.payload) }, + itemUpdated: (state, action: PayloadAction) => { + const index = state.items.findIndex(item => item.id === action.payload.id) + + if (index !== -1) { + state.items[index] = action.payload + } + }, setAddItemRequestStatus: (state, action: PayloadAction) => { state.addItemRequestStatus = action.payload }, + setEditItemRequestStatus: (state, action: PayloadAction) => { + state.editItemRequestStatus = action.payload + }, }, }) const { fetchedItems, - itemReceived, setAddItemRequestStatus, + setEditItemRequestStatus, + itemReceived, + itemUpdated, } = rules.actions export { fetchedItems, setAddItemRequestStatus, + setEditItemRequestStatus, } export default rules.reducer diff --git a/src/styles/rules/form.css b/src/styles/rules/form.css index d4cc7bb..fd16742 100644 --- a/src/styles/rules/form.css +++ b/src/styles/rules/form.css @@ -12,13 +12,13 @@ box-sizing: border-box; } - .error-message { - color: red; - font-size: 0.875rem; - margin-top: 0.25rem; - } - button { - margin-inline: auto; + margin-inline: auto; } } + +.error-message { + color: red; + font-size: 0.875rem; + margin-top: 0.25rem; +} From 7f057e317902eb4263ded3c60ed2b8a581382fb4 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Thu, 24 Apr 2025 14:01:44 +0200 Subject: [PATCH 5/5] feat: add delete item functionality and update request status handling --- src/components/rules/ItemsList.tsx | 15 +++++++++++++-- src/modules/rules.ts | 20 ++++++++++++++++++++ src/styles/rules/form.css | 1 + 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/components/rules/ItemsList.tsx b/src/components/rules/ItemsList.tsx index b58714c..32ea07d 100644 --- a/src/components/rules/ItemsList.tsx +++ b/src/components/rules/ItemsList.tsx @@ -1,4 +1,4 @@ -import { RootState } from '@/store' +import { RootState, useAppDispatch } from '@/store' import numberFormat from '@/tests/numberFormat' import { TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, IconButton, Fab } from '@mui/material' import EditIcon from '@mui/icons-material/Edit' @@ -6,11 +6,20 @@ import DeleteIcon from '@mui/icons-material/Delete' import AddIcon from '@mui/icons-material/Add' import { useSelector } from 'react-redux' import { useNavigate } from 'react-router' +import { deleteItem } from '@/modules/rules' +import { Item, RequestStatus } from '@/types' export function ItemsList() { - const items = useSelector((state: RootState) => state.rules.items) + const dispatch = useAppDispatch() const navigate = useNavigate() + const items = useSelector((state: RootState) => state.rules.items) + const requestStatus = useSelector((state: RootState) => state.rules.deleteItemRequestStatus) + + const handleDelete = (item: Item) => { + dispatch(deleteItem(item.id)) + } + return ( <> @@ -39,6 +48,8 @@ export function ItemsList() { handleDelete(item)} > diff --git a/src/modules/rules.ts b/src/modules/rules.ts index b0559b3..3f51733 100644 --- a/src/modules/rules.ts +++ b/src/modules/rules.ts @@ -6,12 +6,14 @@ type RulesState = { items: Item[] addItemRequestStatus: TRequestStatus editItemRequestStatus: TRequestStatus + deleteItemRequestStatus: TRequestStatus } const INITIAL_STATE: RulesState = { items: [], addItemRequestStatus: RequestStatus.Idle, editItemRequestStatus: RequestStatus.Idle, + deleteItemRequestStatus: RequestStatus.Idle, } // Side Effects / thunks @@ -66,6 +68,17 @@ export const editItem = createAsyncThunk( }, ) +export const deleteItem = createAsyncThunk( + 'rules/deleteItem', + async (itemId: number, { dispatch }) => { + await fetch(`${import.meta.env.VITE_API_URL}/api/shop/items/${itemId}`, { + method: 'DELETE', + }) + + dispatch(itemDeleted(itemId)) + }, +) + const rules = createSlice({ name: 'rule', initialState: INITIAL_STATE, @@ -83,12 +96,18 @@ const rules = createSlice({ state.items[index] = action.payload } }, + itemDeleted: (state, action: PayloadAction) => { + state.items = state.items.filter(item => item.id !== action.payload) + }, setAddItemRequestStatus: (state, action: PayloadAction) => { state.addItemRequestStatus = action.payload }, setEditItemRequestStatus: (state, action: PayloadAction) => { state.editItemRequestStatus = action.payload }, + setDeleteItemRequestStatus: (state, action: PayloadAction) => { + state.deleteItemRequestStatus = action.payload + }, }, }) @@ -98,6 +117,7 @@ const { setEditItemRequestStatus, itemReceived, itemUpdated, + itemDeleted, } = rules.actions export { diff --git a/src/styles/rules/form.css b/src/styles/rules/form.css index fd16742..40a4fe8 100644 --- a/src/styles/rules/form.css +++ b/src/styles/rules/form.css @@ -10,6 +10,7 @@ border: 1px solid #ccc; border-radius: 0.25rem; box-sizing: border-box; + background-color: white; } button {