diff --git a/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.test.tsx b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6c244fbb7398a026c616c2497bc3604933f89a39 --- /dev/null +++ b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.test.tsx @@ -0,0 +1,76 @@ +import { StoreType } from '@/redux/store'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { + InitialStoreState, + getReduxWrapperWithStore, +} from '@/utils/testing/getReduxWrapperWithStore'; +import { act } from 'react-dom/test-utils'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { MODAL_INITIAL_STATE_MOCK } from '@/redux/modal/modal.mock'; +import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component'; + +mockNetworkResponse(); +const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { + const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); + + return ( + render( + <Wrapper> + <AddCommentModal /> + </Wrapper>, + ), + { + store, + } + ); +}; + +describe('AddCommentModal - component', () => { + test('renders AddCommentModal component', () => { + renderComponent(); + + const emailInput = screen.getByLabelText(/email/i); + const contentInput = screen.getByLabelText(/content/i); + expect(emailInput).toBeInTheDocument(); + expect(contentInput).toBeInTheDocument(); + }); + + test('handles input change correctly', () => { + renderComponent(); + + const emailInput: HTMLInputElement = screen.getByLabelText(/email/i); + const contentInput: HTMLInputElement = screen.getByLabelText(/content/i); + + fireEvent.change(emailInput, { target: { value: 'test@email.pl' } }); + fireEvent.change(contentInput, { target: { value: 'test content' } }); + + expect(emailInput.value).toBe('test@email.pl'); + expect(contentInput.value).toBe('test content'); + }); + + test('submits form', async () => { + const { store } = renderComponent({ + modal: { + ...MODAL_INITIAL_STATE_MOCK, + isOpen: true, + modalName: 'add-comment', + }, + }); + + const emailInput: HTMLInputElement = screen.getByLabelText(/email/i); + const contentInput: HTMLInputElement = screen.getByLabelText(/content/i); + const submitButton = screen.getByText(/submit/i); + + fireEvent.change(emailInput, { target: { value: 'test@email.pl' } }); + fireEvent.change(contentInput, { target: { value: 'test content' } }); + act(() => { + submitButton.click(); + }); + + await waitFor(() => { + const modalState = store.getState().modal; + expect(modalState.isOpen).toBeFalsy(); + expect(modalState.modalName).toBe('none'); + }); + }); +}); diff --git a/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7d4926025e73ab8674dd60f74de1f6782d7b3ab6 --- /dev/null +++ b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx @@ -0,0 +1,61 @@ +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { Button } from '@/shared/Button'; +import { Input } from '@/shared/Input'; +import React from 'react'; + +import { addComment } from '@/redux/comment/thunks/addComment'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { currentModelIdSelector } from '@/redux/models/models.selectors'; +import { closeModal } from '@/redux/modal/modal.slice'; + +export const AddCommentModal: React.FC = () => { + const dispatch = useAppDispatch(); + const modelId = useAppSelector(currentModelIdSelector); + + const [data, setData] = React.useState({ email: '', content: '', modelId }); + + const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => { + const { name, value } = e.target; + setData(prevData => ({ ...prevData, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => { + e.preventDefault(); + await dispatch(addComment(data)); + dispatch(closeModal()); + }; + + return ( + <div className="w-[400px] border border-t-[#E1E0E6] bg-white p-[24px]"> + <form onSubmit={handleSubmit}> + <label className="mb-5 block text-sm font-semibold" htmlFor="email"> + Email (visible only to moderators): + <Input + type="text" + name="email" + id="email" + placeholder="Your email here..." + value={data.email} + onChange={handleChange} + className="mt-2.5 text-sm font-medium text-font-400" + /> + </label> + <label className="text-sm font-semibold" htmlFor="content"> + Content: + <Input + type="textarea" + name="content" + id="content" + placeholder="Message here..." + value={data.content} + onChange={handleChange} + className="mt-2.5 text-sm font-medium text-font-400" + /> + </label> + <Button type="submit" className="w-full justify-center text-base font-medium"> + Submit + </Button> + </form> + </div> + ); +}; diff --git a/src/models/fixtures/commentFixture.ts b/src/models/fixtures/commentFixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..a28e2618e273c2cdf736be26c231cfce0ba5a53a --- /dev/null +++ b/src/models/fixtures/commentFixture.ts @@ -0,0 +1,8 @@ +import { ZOD_SEED } from '@/constants'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { commentSchema } from '@/models/commentSchema'; + +export const commentFixture = createFixture(commentSchema, { + seed: ZOD_SEED, +}); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index d5a42e1ccc9f53ad68f2f0019108fb123d20d571..822bce33141d58cb143a4658702d22541b8ba9db 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -99,4 +99,5 @@ export const apiPath = { logout: (): string => `doLogout`, userPrivileges: (login: string): string => `users/${login}?columns=privileges`, getComments: (): string => `projects/${PROJECT_ID}/comments/models/*/`, + addComment: (modelId: number): string => `projects/${PROJECT_ID}/comments/models/${modelId}/`, }; diff --git a/src/redux/comment/comment.types.ts b/src/redux/comment/comment.types.ts index 581da6e4959f40728d402d1dcf51750c731cb15b..368782e8fbf8d5f7c2c0dc117e7eb4e39ae4c606 100644 --- a/src/redux/comment/comment.types.ts +++ b/src/redux/comment/comment.types.ts @@ -15,3 +15,9 @@ export type GetElementProps = { elementId: number; modelId: number; }; + +export type AddCommentProps = { + email: string; + content: string; + modelId: number; +}; diff --git a/src/redux/comment/thunks/addComment.ts b/src/redux/comment/thunks/addComment.ts new file mode 100644 index 0000000000000000000000000000000000000000..37afcf2c86fe6b9b8a7eaa80f558f19a79339e20 --- /dev/null +++ b/src/redux/comment/thunks/addComment.ts @@ -0,0 +1,24 @@ +import { commentSchema } from '@/models/commentSchema'; +import { apiPath } from '@/redux/apiPath'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { ThunkConfig } from '@/types/store'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { Comment } from '@/types/models'; +import { AddCommentProps } from '@/redux/comment/comment.types'; + +export const addComment = createAsyncThunk<Comment | null, AddCommentProps, ThunkConfig>( + 'project/getComments', + async ({ email, content, modelId }) => { + try { + const payload = { email, content }; + const response = await axiosInstance.post<Comment>(apiPath.addComment(modelId), payload); + + const isDataValid = validateDataUsingZodSchema(response.data, commentSchema); + + return isDataValid ? response.data : null; + } catch (error) { + return Promise.reject(error); + } + }, +); diff --git a/src/types/modal.ts b/src/types/modal.ts index b268bf17ab32baf80f7adda78c5770c0bc482578..12e82d482a54a391a5efd2d76ebe185a1c31b522 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -1,5 +1,6 @@ export type ModalName = | 'none' + | 'add-comment' | 'overview-images' | 'mol-art' | 'login'