From bf3097500a3269eda03c10f9fc193d9b8abf8a0c Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 1 Jun 2023 19:49:37 +0200 Subject: [PATCH] Lucas/t 223 i can add comments to companies or people using the right (#181) * wip * Implemented comment input text component * Improved behavior --- front/package-lock.json | 54 ++++++++ front/package.json | 1 + front/src/components/buttons/IconButton.tsx | 36 ++++++ .../components/comments/CommentTextInput.tsx | 122 ++++++++++++++++++ .../comments/RightDrawerComments.tsx | 14 +- .../__stories__/CommentTextInput.stories.tsx | 30 +++++ .../layout/right-drawer/RightDrawerBody.tsx | 8 ++ .../layout/right-drawer/RightDrawerPage.tsx | 8 ++ .../layout/right-drawer/RightDrawerTopBar.tsx | 1 - front/src/layout/styles/themes.ts | 4 + .../__stories__/Companies.stories.tsx | 7 +- .../src/testing/ComponentStorybookLayout.tsx | 21 +++ front/src/testing/renderWrappers.tsx | 39 ++++++ 13 files changed, 339 insertions(+), 6 deletions(-) create mode 100644 front/src/components/buttons/IconButton.tsx create mode 100644 front/src/components/comments/CommentTextInput.tsx create mode 100644 front/src/components/comments/__stories__/CommentTextInput.stories.tsx create mode 100644 front/src/layout/right-drawer/RightDrawerBody.tsx create mode 100644 front/src/layout/right-drawer/RightDrawerPage.tsx create mode 100644 front/src/testing/ComponentStorybookLayout.tsx create mode 100644 front/src/testing/renderWrappers.tsx diff --git a/front/package-lock.json b/front/package-lock.json index 61b920c44..a11517dad 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -26,6 +26,7 @@ "react-hotkeys-hook": "^4.4.0", "react-icons": "^4.8.0", "react-router-dom": "^6.4.4", + "react-textarea-autosize": "^8.4.1", "recoil": "^0.7.7", "uuid": "^9.0.0", "web-vitals": "^2.1.4" @@ -31481,6 +31482,22 @@ "node": ">=10" } }, + "node_modules/react-textarea-autosize": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.1.tgz", + "integrity": "sha512-aD2C+qK6QypknC+lCMzteOdIjoMbNlgSFmJjCV+DrfTPwp59i/it9mMNf2HDzvRjQgKAyBDPyLJhcrzElf2U4Q==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -34999,6 +35016,43 @@ "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==", "dev": true }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-resize-observer": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", diff --git a/front/package.json b/front/package.json index 3b25cf2fc..b6dbe5530 100644 --- a/front/package.json +++ b/front/package.json @@ -21,6 +21,7 @@ "react-hotkeys-hook": "^4.4.0", "react-icons": "^4.8.0", "react-router-dom": "^6.4.4", + "react-textarea-autosize": "^8.4.1", "recoil": "^0.7.7", "uuid": "^9.0.0", "web-vitals": "^2.1.4" diff --git a/front/src/components/buttons/IconButton.tsx b/front/src/components/buttons/IconButton.tsx new file mode 100644 index 000000000..0c20a977c --- /dev/null +++ b/front/src/components/buttons/IconButton.tsx @@ -0,0 +1,36 @@ +import styled from '@emotion/styled'; + +const StyledIconButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + + width: 20px; + height: 20px; + + padding: 0; + border: none; + border-radius: 50%; + + background: ${(props) => props.theme.text80}; + color: ${(props) => props.theme.text100}; + + transition: color 0.1s ease-in-out, background 0.1s ease-in-out; + + background: ${(props) => props.theme.blue}; + color: ${(props) => props.theme.text0}; + cursor: pointer; + + &:disabled { + background: ${(props) => props.theme.quadraryBackground}; + color: ${(props) => props.theme.text80}; + cursor: default; + } +`; + +export function IconButton({ + icon, + ...props +}: { icon: React.ReactNode } & React.ButtonHTMLAttributes) { + return {icon}; +} diff --git a/front/src/components/comments/CommentTextInput.tsx b/front/src/components/comments/CommentTextInput.tsx new file mode 100644 index 000000000..f9e5f8c14 --- /dev/null +++ b/front/src/components/comments/CommentTextInput.tsx @@ -0,0 +1,122 @@ +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { HiArrowSmRight } from 'react-icons/hi'; +import TextareaAutosize from 'react-textarea-autosize'; +import { IconButton } from '../buttons/IconButton'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { HotkeysEvent } from 'react-hotkeys-hook/dist/types'; + +type OwnProps = { + onSend?: (text: string) => void; + placeholder?: string; +}; + +const StyledContainer = styled.div` + display: flex; + min-height: 32px; + width: 100%; +`; + +const StyledTextArea = styled(TextareaAutosize)` + width: 100%; + padding: 8px; + font-size: 13px; + font-family: inherit; + font-weight: 400; + line-height: 16px; + border: none; + border-radius: 5px; + background: ${(props) => props.theme.tertiaryBackground}; + color: ${(props) => props.theme.text80}; + overflow: auto; + resize: none; + + &:focus { + outline: none; + border: none; + } + + &::placeholder { + color: ${(props) => props.theme.text30}; + font-weight: 400; + } +`; + +const StyledBottomRightIconButton = styled.div` + width: 0px; + position: relative; + top: calc(100% - 26.5px); + right: 26px; +`; + +export function CommentTextInput({ placeholder, onSend }: OwnProps) { + const [text, setText] = useState(''); + + const isSendButtonDisabled = !text; + + useHotkeys( + ['shift+enter', 'enter'], + (event: KeyboardEvent, handler: HotkeysEvent) => { + if (handler.shift) { + return; + } else { + event.preventDefault(); + + onSend?.(text); + + setText(''); + } + }, + { + enableOnContentEditable: true, + enableOnFormTags: true, + }, + [onSend], + ); + + useHotkeys( + 'esc', + (event: KeyboardEvent, handler: HotkeysEvent) => { + event.preventDefault(); + + setText(''); + }, + { + enableOnContentEditable: true, + enableOnFormTags: true, + }, + [onSend], + ); + + function handleInputChange(event: React.FormEvent) { + const newText = event.currentTarget.value; + + setText(newText); + } + + function handleOnClickSendButton() { + onSend?.(text); + + setText(''); + } + + return ( + <> + + + + } + disabled={isSendButtonDisabled} + /> + + + + ); +} diff --git a/front/src/components/comments/RightDrawerComments.tsx b/front/src/components/comments/RightDrawerComments.tsx index 13372bc79..1788a06a9 100644 --- a/front/src/components/comments/RightDrawerComments.tsx +++ b/front/src/components/comments/RightDrawerComments.tsx @@ -1,9 +1,19 @@ +import { RightDrawerBody } from '../../layout/right-drawer/RightDrawerBody'; +import { RightDrawerPage } from '../../layout/right-drawer/RightDrawerPage'; import { RightDrawerTopBar } from '../../layout/right-drawer/RightDrawerTopBar'; +import { CommentTextInput } from './CommentTextInput'; export function RightDrawerComments() { + function handleSendComment(text: string) { + console.log(text); + } + return ( - <> + - + + + + ); } diff --git a/front/src/components/comments/__stories__/CommentTextInput.stories.tsx b/front/src/components/comments/__stories__/CommentTextInput.stories.tsx new file mode 100644 index 000000000..2a7f7ff54 --- /dev/null +++ b/front/src/components/comments/__stories__/CommentTextInput.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { graphqlMocks } from '../../../testing/graphqlMocks'; +import { CommentTextInput } from '../CommentTextInput'; +import { getRenderWrapperForComponent } from '../../../testing/renderWrappers'; + +const meta: Meta = { + title: 'Components/CommentTextInput', + component: CommentTextInput, + argTypes: { + onSend: { + action: 'onSend', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: getRenderWrapperForComponent(), + parameters: { + msw: graphqlMocks, + actions: { argTypesRegex: '^on.*' }, + }, + args: { + onSend: (text: string) => { + console.log(text); + }, + }, +}; diff --git a/front/src/layout/right-drawer/RightDrawerBody.tsx b/front/src/layout/right-drawer/RightDrawerBody.tsx new file mode 100644 index 000000000..b946f87a5 --- /dev/null +++ b/front/src/layout/right-drawer/RightDrawerBody.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +export const RightDrawerBody = styled.div` + display: flex; + flex-direction: column; + + padding: 8px; +`; diff --git a/front/src/layout/right-drawer/RightDrawerPage.tsx b/front/src/layout/right-drawer/RightDrawerPage.tsx new file mode 100644 index 000000000..852952e7f --- /dev/null +++ b/front/src/layout/right-drawer/RightDrawerPage.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +export const RightDrawerPage = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +`; diff --git a/front/src/layout/right-drawer/RightDrawerTopBar.tsx b/front/src/layout/right-drawer/RightDrawerTopBar.tsx index fcb0ba7ff..e02e4855c 100644 --- a/front/src/layout/right-drawer/RightDrawerTopBar.tsx +++ b/front/src/layout/right-drawer/RightDrawerTopBar.tsx @@ -12,7 +12,6 @@ const StyledRightDrawerTopBar = styled.div` font-size: 13px; color: ${(props) => props.theme.text60}; border-bottom: 1px solid ${(props) => props.theme.lightBorder}; - width: 100%; `; const StyledTopBarTitle = styled.div` diff --git a/front/src/layout/styles/themes.ts b/front/src/layout/styles/themes.ts index d1ba75145..5024885a8 100644 --- a/front/src/layout/styles/themes.ts +++ b/front/src/layout/styles/themes.ts @@ -27,6 +27,8 @@ const lightThemeSpecific = { primaryBackground: '#fff', secondaryBackground: '#fcfcfc', tertiaryBackground: '#f5f5f5', + quadraryBackground: '#ebebeb', + pinkBackground: '#ffe5f4', greenBackground: '#e6fff2', purpleBackground: '#e0e0ff', @@ -64,6 +66,8 @@ const darkThemeSpecific: typeof lightThemeSpecific = { primaryBackground: '#141414', secondaryBackground: '#171717', tertiaryBackground: '#333333', + quadraryBackground: '#444444', + pinkBackground: '#cc0078', greenBackground: '#1e7e50', purpleBackground: '#1111b7', diff --git a/front/src/pages/companies/__stories__/Companies.stories.tsx b/front/src/pages/companies/__stories__/Companies.stories.tsx index 9441c91b4..5ea914038 100644 --- a/front/src/pages/companies/__stories__/Companies.stories.tsx +++ b/front/src/pages/companies/__stories__/Companies.stories.tsx @@ -2,7 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import Companies from '../Companies'; -import { render, mocks } from './shared'; +import { getRenderWrapperForPage } from '../../../testing/renderWrappers'; +import { graphqlMocks } from '../../../testing/graphqlMocks'; const meta: Meta = { title: 'Pages/Companies', @@ -14,8 +15,8 @@ export default meta; export type Story = StoryObj; export const Default: Story = { - render, + render: getRenderWrapperForPage(), parameters: { - msw: mocks, + msw: graphqlMocks, }, }; diff --git a/front/src/testing/ComponentStorybookLayout.tsx b/front/src/testing/ComponentStorybookLayout.tsx new file mode 100644 index 000000000..f7a0e1a79 --- /dev/null +++ b/front/src/testing/ComponentStorybookLayout.tsx @@ -0,0 +1,21 @@ +import styled from '@emotion/styled'; + +const StyledLayout = styled.div` + display: flex; + flex-direction: row; + width: fit-content; + height: fit-content; + + padding: 20px; + background: ${(props) => props.theme.primaryBackground}; + border-radius: 5px; + box-shadow: 0px 0px 2px; +`; + +type OwnProps = { + children: JSX.Element; +}; + +export function ComponentStorybookLayout({ children }: OwnProps) { + return {children}; +} diff --git a/front/src/testing/renderWrappers.tsx b/front/src/testing/renderWrappers.tsx new file mode 100644 index 000000000..4f609fd05 --- /dev/null +++ b/front/src/testing/renderWrappers.tsx @@ -0,0 +1,39 @@ +import { MemoryRouter } from 'react-router-dom'; +import { FullHeightStorybookLayout } from './FullHeightStorybookLayout'; +import { ThemeProvider } from '@emotion/react'; +import { ApolloProvider } from '@apollo/client'; +import { RecoilRoot } from 'recoil'; +import { mockedClient } from './mockedClient'; +import { lightTheme } from '../layout/styles/themes'; +import React from 'react'; +import { ComponentStorybookLayout } from './ComponentStorybookLayout'; + +export function getRenderWrapperForPage(children: React.ReactElement) { + return function render() { + return ( + + + + + {children} + + + + + ); + }; +} + +export function getRenderWrapperForComponent(children: React.ReactElement) { + return function render() { + return ( + + + + {children} + + + + ); + }; +}