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-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",
|
||||
|
||||
@ -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"
|
||||
|
||||
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 { CommentTextInput } from './CommentTextInput';
|
||||
|
||||
export function RightDrawerComments() {
|
||||
function handleSendComment(text: string) {
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RightDrawerPage>
|
||||
<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;
|
||||
color: ${(props) => props.theme.text60};
|
||||
border-bottom: 1px solid ${(props) => props.theme.lightBorder};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTopBarTitle = styled.div`
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<typeof Companies> = {
|
||||
title: 'Pages/Companies',
|
||||
@ -14,8 +15,8 @@ export default meta;
|
||||
export type Story = StoryObj<typeof Companies>;
|
||||
|
||||
export const Default: Story = {
|
||||
render,
|
||||
render: getRenderWrapperForPage(<Companies />),
|
||||
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