Lucas/t 223 i can add comments to companies or people using the right (#181)
* wip * Implemented comment input text component * Improved behavior
This commit is contained in:
54
front/package-lock.json
generated
54
front/package-lock.json
generated
@ -26,6 +26,7 @@
|
|||||||
"react-hotkeys-hook": "^4.4.0",
|
"react-hotkeys-hook": "^4.4.0",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-router-dom": "^6.4.4",
|
"react-router-dom": "^6.4.4",
|
||||||
|
"react-textarea-autosize": "^8.4.1",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
@ -31481,6 +31482,22 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@ -34999,6 +35016,43 @@
|
|||||||
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==",
|
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/use-resize-observer": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz",
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
"react-hotkeys-hook": "^4.4.0",
|
"react-hotkeys-hook": "^4.4.0",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.8.0",
|
||||||
"react-router-dom": "^6.4.4",
|
"react-router-dom": "^6.4.4",
|
||||||
|
"react-textarea-autosize": "^8.4.1",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
|||||||
36
front/src/components/buttons/IconButton.tsx
Normal file
36
front/src/components/buttons/IconButton.tsx
Normal file
@ -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<HTMLButtonElement>) {
|
||||||
|
return <StyledIconButton {...props}>{icon}</StyledIconButton>;
|
||||||
|
}
|
||||||
122
front/src/components/comments/CommentTextInput.tsx
Normal file
122
front/src/components/comments/CommentTextInput.tsx
Normal file
@ -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<HTMLTextAreaElement>) {
|
||||||
|
const newText = event.currentTarget.value;
|
||||||
|
|
||||||
|
setText(newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnClickSendButton() {
|
||||||
|
onSend?.(text);
|
||||||
|
|
||||||
|
setText('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledTextArea
|
||||||
|
placeholder={placeholder || 'Write something...'}
|
||||||
|
maxRows={5}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={text}
|
||||||
|
/>
|
||||||
|
<StyledBottomRightIconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleOnClickSendButton}
|
||||||
|
icon={<HiArrowSmRight size={15} />}
|
||||||
|
disabled={isSendButtonDisabled}
|
||||||
|
/>
|
||||||
|
</StyledBottomRightIconButton>
|
||||||
|
</StyledContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 { RightDrawerTopBar } from '../../layout/right-drawer/RightDrawerTopBar';
|
||||||
|
import { CommentTextInput } from './CommentTextInput';
|
||||||
|
|
||||||
export function RightDrawerComments() {
|
export function RightDrawerComments() {
|
||||||
|
function handleSendComment(text: string) {
|
||||||
|
console.log(text);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RightDrawerPage>
|
||||||
<RightDrawerTopBar title="Comments" />
|
<RightDrawerTopBar title="Comments" />
|
||||||
</>
|
<RightDrawerBody>
|
||||||
|
<CommentTextInput onSend={handleSendComment} />
|
||||||
|
</RightDrawerBody>
|
||||||
|
</RightDrawerPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<typeof CommentTextInput> = {
|
||||||
|
title: 'Components/CommentTextInput',
|
||||||
|
component: CommentTextInput,
|
||||||
|
argTypes: {
|
||||||
|
onSend: {
|
||||||
|
action: 'onSend',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof CommentTextInput>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: getRenderWrapperForComponent(<CommentTextInput />),
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
actions: { argTypesRegex: '^on.*' },
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
onSend: (text: string) => {
|
||||||
|
console.log(text);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
8
front/src/layout/right-drawer/RightDrawerBody.tsx
Normal file
8
front/src/layout/right-drawer/RightDrawerBody.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const RightDrawerBody = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 8px;
|
||||||
|
`;
|
||||||
8
front/src/layout/right-drawer/RightDrawerPage.tsx
Normal file
8
front/src/layout/right-drawer/RightDrawerPage.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const RightDrawerPage = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
||||||
@ -12,7 +12,6 @@ const StyledRightDrawerTopBar = styled.div`
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: ${(props) => props.theme.text60};
|
color: ${(props) => props.theme.text60};
|
||||||
border-bottom: 1px solid ${(props) => props.theme.lightBorder};
|
border-bottom: 1px solid ${(props) => props.theme.lightBorder};
|
||||||
width: 100%;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTopBarTitle = styled.div`
|
const StyledTopBarTitle = styled.div`
|
||||||
|
|||||||
@ -27,6 +27,8 @@ const lightThemeSpecific = {
|
|||||||
primaryBackground: '#fff',
|
primaryBackground: '#fff',
|
||||||
secondaryBackground: '#fcfcfc',
|
secondaryBackground: '#fcfcfc',
|
||||||
tertiaryBackground: '#f5f5f5',
|
tertiaryBackground: '#f5f5f5',
|
||||||
|
quadraryBackground: '#ebebeb',
|
||||||
|
|
||||||
pinkBackground: '#ffe5f4',
|
pinkBackground: '#ffe5f4',
|
||||||
greenBackground: '#e6fff2',
|
greenBackground: '#e6fff2',
|
||||||
purpleBackground: '#e0e0ff',
|
purpleBackground: '#e0e0ff',
|
||||||
@ -64,6 +66,8 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
|
|||||||
primaryBackground: '#141414',
|
primaryBackground: '#141414',
|
||||||
secondaryBackground: '#171717',
|
secondaryBackground: '#171717',
|
||||||
tertiaryBackground: '#333333',
|
tertiaryBackground: '#333333',
|
||||||
|
quadraryBackground: '#444444',
|
||||||
|
|
||||||
pinkBackground: '#cc0078',
|
pinkBackground: '#cc0078',
|
||||||
greenBackground: '#1e7e50',
|
greenBackground: '#1e7e50',
|
||||||
purpleBackground: '#1111b7',
|
purpleBackground: '#1111b7',
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|||||||
|
|
||||||
import Companies from '../Companies';
|
import Companies from '../Companies';
|
||||||
|
|
||||||
import { render, mocks } from './shared';
|
import { getRenderWrapperForPage } from '../../../testing/renderWrappers';
|
||||||
|
import { graphqlMocks } from '../../../testing/graphqlMocks';
|
||||||
|
|
||||||
const meta: Meta<typeof Companies> = {
|
const meta: Meta<typeof Companies> = {
|
||||||
title: 'Pages/Companies',
|
title: 'Pages/Companies',
|
||||||
@ -14,8 +15,8 @@ export default meta;
|
|||||||
export type Story = StoryObj<typeof Companies>;
|
export type Story = StoryObj<typeof Companies>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render,
|
render: getRenderWrapperForPage(<Companies />),
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: mocks,
|
msw: graphqlMocks,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
21
front/src/testing/ComponentStorybookLayout.tsx
Normal file
21
front/src/testing/ComponentStorybookLayout.tsx
Normal file
@ -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 <StyledLayout>{children}</StyledLayout>;
|
||||||
|
}
|
||||||
39
front/src/testing/renderWrappers.tsx
Normal file
39
front/src/testing/renderWrappers.tsx
Normal file
@ -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 (
|
||||||
|
<RecoilRoot>
|
||||||
|
<ApolloProvider client={mockedClient}>
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<FullHeightStorybookLayout>{children}</FullHeightStorybookLayout>
|
||||||
|
</MemoryRouter>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ApolloProvider>
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRenderWrapperForComponent(children: React.ReactElement) {
|
||||||
|
return function render() {
|
||||||
|
return (
|
||||||
|
<RecoilRoot>
|
||||||
|
<ApolloProvider client={mockedClient}>
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<ComponentStorybookLayout>{children}</ComponentStorybookLayout>
|
||||||
|
</ThemeProvider>
|
||||||
|
</ApolloProvider>
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user