@abgov/jsonforms-components 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +36 -0
  3. package/.releaserc.json +25 -0
  4. package/README.md +251 -0
  5. package/jest.config.ts +11 -0
  6. package/package.json +17 -0
  7. package/project.json +55 -0
  8. package/rollup.config.js +14 -0
  9. package/src/index.ts +166 -0
  10. package/src/lib/Additional/HelpContent.tsx +95 -0
  11. package/src/lib/Additional/index.ts +1 -0
  12. package/src/lib/Additional/styled-components.ts +27 -0
  13. package/src/lib/Cells/DateCell.tsx +10 -0
  14. package/src/lib/Cells/IntegerCell.tsx +10 -0
  15. package/src/lib/Cells/NumberCell.tsx +10 -0
  16. package/src/lib/Cells/TextCell.tsx +10 -0
  17. package/src/lib/Cells/TimeCell.tsx +10 -0
  18. package/src/lib/Cells/index.tsx +14 -0
  19. package/src/lib/Context/index.tsx +172 -0
  20. package/src/lib/Controls/FileUploader/ContextMenu.tsx +50 -0
  21. package/src/lib/Controls/FileUploader/FileUploaderControl.tsx +131 -0
  22. package/src/lib/Controls/FileUploader/FileUploaderTester.tsx +3 -0
  23. package/src/lib/Controls/FileUploader/index.tsx +2 -0
  24. package/src/lib/Controls/FileUploader/styled-components.tsx +10 -0
  25. package/src/lib/Controls/FormStepper/FormStepperControl.tsx +269 -0
  26. package/src/lib/Controls/FormStepper/FormStepperTester.tsx +22 -0
  27. package/src/lib/Controls/FormStepper/index.tsx +2 -0
  28. package/src/lib/Controls/FormStepper/styled-components.tsx +17 -0
  29. package/src/lib/Controls/Inputs/InputBaseControl.tsx +52 -0
  30. package/src/lib/Controls/Inputs/InputBooleanControl.tsx +67 -0
  31. package/src/lib/Controls/Inputs/InputBooleanRadioControl.tsx +74 -0
  32. package/src/lib/Controls/Inputs/InputDateControl.tsx +90 -0
  33. package/src/lib/Controls/Inputs/InputDateTimeControl.tsx +46 -0
  34. package/src/lib/Controls/Inputs/InputEnum.tsx +74 -0
  35. package/src/lib/Controls/Inputs/InputEnumAutoComplete.tsx +73 -0
  36. package/src/lib/Controls/Inputs/InputEnumRadios.tsx +43 -0
  37. package/src/lib/Controls/Inputs/InputIntegerControl.tsx +63 -0
  38. package/src/lib/Controls/Inputs/InputMultiLineTextControl.tsx +63 -0
  39. package/src/lib/Controls/Inputs/InputNumberControl.tsx +63 -0
  40. package/src/lib/Controls/Inputs/InputTextControl.tsx +62 -0
  41. package/src/lib/Controls/Inputs/InputTimeControl.tsx +46 -0
  42. package/src/lib/Controls/Inputs/index.tsx +13 -0
  43. package/src/lib/Controls/Inputs/inputControl.spec.ts +84 -0
  44. package/src/lib/Controls/Inputs/type.ts +3 -0
  45. package/src/lib/Controls/ObjectArray/DeleteDialog.tsx +49 -0
  46. package/src/lib/Controls/ObjectArray/ObjectArray.tsx +59 -0
  47. package/src/lib/Controls/ObjectArray/ObjectArrayToolBar.tsx +51 -0
  48. package/src/lib/Controls/ObjectArray/ObjectListControl.tsx +368 -0
  49. package/src/lib/Controls/ObjectArray/index.tsx +1 -0
  50. package/src/lib/Controls/ObjectArray/styled-components.tsx +13 -0
  51. package/src/lib/Controls/index.tsx +4 -0
  52. package/src/lib/ErrorHandling/GoAErrorControl.tsx +53 -0
  53. package/src/lib/ErrorHandling/MessageControl.tsx +19 -0
  54. package/src/lib/ErrorHandling/categorizationValidation.spec.ts +98 -0
  55. package/src/lib/ErrorHandling/controlValildation.spec.ts +57 -0
  56. package/src/lib/ErrorHandling/errorCheck.spec.ts +185 -0
  57. package/src/lib/ErrorHandling/errorCheck.tsx +86 -0
  58. package/src/lib/ErrorHandling/layoutValildation.spec.ts +47 -0
  59. package/src/lib/ErrorHandling/otherValildation.spec.ts +74 -0
  60. package/src/lib/ErrorHandling/schemaValidation.ts +115 -0
  61. package/src/lib/common/Grid.tsx +55 -0
  62. package/src/lib/jsonforms-components.module.scss +7 -0
  63. package/src/lib/jsonforms-components.spec.tsx +10 -0
  64. package/src/lib/jsonforms-components.tsx +14 -0
  65. package/src/lib/layouts/GroupControl.tsx +25 -0
  66. package/src/lib/layouts/HorizontalLayoutControl.tsx +30 -0
  67. package/src/lib/layouts/VerticalLayoutControl.tsx +28 -0
  68. package/src/lib/layouts/index.ts +3 -0
  69. package/src/lib/util/layout.tsx +68 -0
  70. package/src/lib/util/schemaUtils.ts +9 -0
  71. package/src/lib/util/stringUtils.ts +98 -0
  72. package/src/lib/util/style-component.ts +8 -0
  73. package/tsconfig.json +20 -0
  74. package/tsconfig.lib.json +19 -0
  75. package/tsconfig.spec.json +20 -0
@@ -0,0 +1,27 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const HelpContentDiv = styled.div`
4
+ .parent-label {
5
+ font-size: 24px;
6
+ margin-bottom: 1rem;
7
+ font-weight: bold;
8
+ }
9
+
10
+ .child-label {
11
+ font-size: 18px;
12
+ margin-bottom: 0.5rem;
13
+ font-weight: bold;
14
+ }
15
+ .parent-margin {
16
+ margin-bottom: 1.5rem;
17
+ }
18
+ .child-margin {
19
+ margin-bottom: 0.25rem;
20
+ }
21
+ ul {
22
+ margin: 0 0 0 0.5rem;
23
+ }
24
+ .single-line {
25
+ margin: 0.25rem 0 0.25rem 0;
26
+ }
27
+ `;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { CellProps, isDateControl, RankedTester, rankWith, WithClassname } from '@jsonforms/core';
3
+ import { withJsonFormsCellProps } from '@jsonforms/react';
4
+ import { GoADateInput } from '../Controls';
5
+
6
+ export const GoADateCell = (props: CellProps & WithClassname) => <GoADateInput {...props} />;
7
+
8
+ export const GoADateCellTester: RankedTester = rankWith(1, isDateControl);
9
+
10
+ export default withJsonFormsCellProps(GoADateCell);
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { CellProps, isIntegerControl, RankedTester, rankWith, WithClassname } from '@jsonforms/core';
3
+ import { withJsonFormsCellProps } from '@jsonforms/react';
4
+ import { GoAInputInteger } from '../Controls';
5
+
6
+ export const GoAIntegerCell = (props: CellProps & WithClassname) => <GoAInputInteger {...props} />;
7
+
8
+ export const GoAIntegerCellTester: RankedTester = rankWith(1, isIntegerControl);
9
+
10
+ export default withJsonFormsCellProps(GoAIntegerCell);
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { CellProps, isNumberControl, RankedTester, rankWith, WithClassname } from '@jsonforms/core';
3
+ import { withJsonFormsCellProps } from '@jsonforms/react';
4
+ import { GoANumberInput } from '../Controls';
5
+
6
+ export const GoANumberCell = (props: CellProps & WithClassname) => <GoANumberInput {...props} />;
7
+
8
+ export const GoANumberCellTester: RankedTester = rankWith(1, isNumberControl);
9
+
10
+ export default withJsonFormsCellProps(GoANumberCell);
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { CellProps, isStringControl, RankedTester, rankWith, WithClassname } from '@jsonforms/core';
3
+ import { withJsonFormsCellProps } from '@jsonforms/react';
4
+ import { GoAInputText } from '../Controls';
5
+
6
+ export const GoATextCell = (props: CellProps & WithClassname) => <GoAInputText {...props} />;
7
+
8
+ export const GoATextCellTester: RankedTester = rankWith(1, isStringControl);
9
+
10
+ export default withJsonFormsCellProps(GoATextCell);
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { CellProps, isTimeControl, RankedTester, rankWith, WithClassname } from '@jsonforms/core';
3
+ import { withJsonFormsCellProps } from '@jsonforms/react';
4
+ import { GoADateInput } from '../Controls';
5
+
6
+ export const GoATimeCell = (props: CellProps & WithClassname) => <GoADateInput {...props} />;
7
+
8
+ export const GoATimeCellTester: RankedTester = rankWith(2, isTimeControl);
9
+
10
+ export default withJsonFormsCellProps(GoATimeCell);
@@ -0,0 +1,14 @@
1
+ import { GoATextCell, GoATextCellTester } from './TextCell';
2
+ import { GoADateCell, GoADateCellTester } from './DateCell';
3
+ import { GoATimeCell, GoATimeCellTester } from './TimeCell';
4
+ import { GoANumberCell, GoANumberCellTester } from './NumberCell';
5
+ import { GoAIntegerCell, GoAIntegerCellTester } from './IntegerCell';
6
+ import { withJsonFormsCellProps } from '@jsonforms/react';
7
+ import { JsonFormsCellRendererRegistryEntry } from '@jsonforms/core';
8
+ export const InputCells: JsonFormsCellRendererRegistryEntry[] = [
9
+ { tester: GoATextCellTester, cell: withJsonFormsCellProps(GoATextCell) },
10
+ { tester: GoADateCellTester, cell: withJsonFormsCellProps(GoADateCell) },
11
+ { tester: GoATimeCellTester, cell: withJsonFormsCellProps(GoATimeCell) },
12
+ { tester: GoANumberCellTester, cell: withJsonFormsCellProps(GoANumberCell) },
13
+ { tester: GoAIntegerCellTester, cell: withJsonFormsCellProps(GoAIntegerCell) },
14
+ ];
@@ -0,0 +1,172 @@
1
+ import React, { createContext } from 'react';
2
+ import axios, { AxiosRequestConfig, AxiosStatic } from 'axios';
3
+
4
+ interface enumerators {
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ data: Map<string, () => any>;
7
+ functions: Map<string, () => (file: File, propertyId: string) => void>;
8
+ }
9
+
10
+ export interface AllData {
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ [x: string]: any;
13
+ }
14
+
15
+ const getAxiosInterceptorConfig = (axios: AxiosStatic): [number, AxiosStatic] => {
16
+ const requestId = axios.interceptors.request.use((req) => {
17
+ if (req.data === undefined) {
18
+ throw new Error(`The URL: ${req.url} encountered a CORS error.`);
19
+ }
20
+ return req;
21
+ });
22
+
23
+ return [requestId, axios];
24
+ };
25
+
26
+ export function addDataByUrl(key: string, url: string, processDataFunction: (url: string) => string[], token?: string) {
27
+ let header = {} as AxiosRequestConfig<unknown>;
28
+ const [requestId, axiosWithConfig] = getAxiosInterceptorConfig(axios);
29
+ if (token) {
30
+ header = { ...header, ...{ Authorization: `Bearer ${token}` } };
31
+ }
32
+
33
+ axiosWithConfig
34
+ .get(url, header)
35
+ .then((response) => {
36
+ const processedData = processDataFunction(response.data);
37
+ enumValues.set(key, () => processedData);
38
+ })
39
+ .catch((err: Error) => {
40
+ if (err.message.includes('CORS')) {
41
+ console.warn(err.message);
42
+ } else {
43
+ console.warn(`addDataByUrl: ${err.message}`);
44
+ }
45
+ });
46
+ axiosWithConfig.interceptors.request.eject(requestId);
47
+ }
48
+
49
+ export function addDataByOptions(key: string, url: string, location: string[], type: string, values: string[]) {
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ const dataFunction = (data: any) => {
52
+ let dataLink = data;
53
+ let returnData = [''];
54
+
55
+ const locationArray = location && !Array.isArray(location) ? [location] : location;
56
+ locationArray?.forEach((attribute) => {
57
+ dataLink = dataLink[attribute];
58
+ });
59
+
60
+ const valuesArray = Array.isArray(values) ? values : [values];
61
+
62
+ if (type === 'keys') {
63
+ returnData = Object.keys(dataLink);
64
+ } else {
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ returnData = dataLink.map((entry: any) => {
67
+ let parse = '';
68
+ valuesArray.forEach((v, index) => {
69
+ parse += `${entry[v]}`;
70
+
71
+ if (index < valuesArray.length - 1) {
72
+ parse += ' ';
73
+ }
74
+ });
75
+ return parse;
76
+ });
77
+ }
78
+
79
+ return returnData;
80
+ };
81
+
82
+ addDataByUrl(key, url, dataFunction);
83
+ }
84
+
85
+ interface FileManagement {
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ fileList?: any;
88
+ uploadFile?: (file: File, propertyId: string) => void;
89
+ downloadFile?: (file: File) => void;
90
+ deleteFile?: (file: File) => void;
91
+ }
92
+
93
+ type Props = {
94
+ children?: React.ReactNode;
95
+ fileManagement?: FileManagement;
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ data?: any;
98
+ };
99
+
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ const enumValues: Map<string, () => Record<string, any> | string[]> = new Map<string, () => Record<string, any>>();
102
+ const enumFunctions: Map<string, () => (file: File, propertyId: string) => void> = new Map<
103
+ string,
104
+ () => (file: File, propertyId: string) => void
105
+ >();
106
+
107
+ const baseEnumerator = {
108
+ data: enumValues,
109
+ functions: enumFunctions,
110
+ };
111
+
112
+ export const JsonFormContext = createContext(baseEnumerator);
113
+
114
+ export function ContextProvider(props: Props): JSX.Element | null {
115
+ const outerTheme = React.useContext(JsonFormContext);
116
+
117
+ if (props.fileManagement) {
118
+ const { fileList, uploadFile, downloadFile, deleteFile } = props.fileManagement;
119
+
120
+ /* eslint-disable @typescript-eslint/no-empty-function */
121
+ const uploadFileFunction = uploadFile ? uploadFile : () => {};
122
+ const downloadFileFunction = downloadFile ? downloadFile : () => {};
123
+ const deleteFileFunction = deleteFile ? deleteFile : () => {};
124
+
125
+ enumValues.set('file-list', () => fileList);
126
+ enumFunctions.set('upload-file', () => uploadFileFunction);
127
+ enumFunctions.set('download-file', () => downloadFileFunction);
128
+ enumFunctions.set('delete-file', () => deleteFileFunction);
129
+ }
130
+
131
+ if (props.data) {
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ props.data?.forEach((item: any) => {
134
+ enumValues.set(Object.keys(item)[0], () => item);
135
+ });
136
+ }
137
+
138
+ if (!props.children) {
139
+ return null;
140
+ }
141
+
142
+ return <JsonFormContext.Provider value={baseEnumerator}>{props.children}</JsonFormContext.Provider>;
143
+ }
144
+
145
+ /**
146
+ * Grabs data stored under a given key
147
+ *
148
+ */
149
+ export function getData(key: string) {
150
+ const dataFunction = baseEnumerator.data.get(key);
151
+ return dataFunction && dataFunction();
152
+ }
153
+
154
+ /**
155
+ * Grabs all data
156
+ *
157
+ */
158
+ export function getAllData() {
159
+ const allData: AllData = [];
160
+ baseEnumerator.data.forEach((d, key) => {
161
+ allData.push({ [key]: d() });
162
+ });
163
+ return allData;
164
+ }
165
+ /**
166
+ * Allows additional data to be added under a given key
167
+ *
168
+ * This data will then be available inside the context
169
+ */
170
+ export function addData(key: string, data: Record<string, unknown> | unknown[]) {
171
+ enumValues.set(key, () => data);
172
+ }
@@ -0,0 +1,50 @@
1
+ import React, { FC } from 'react';
2
+ import { GoAButton, GoAIconButton, GoAIconType } from '@abgov/react-components-new';
3
+ import styled from 'styled-components';
4
+
5
+ interface ContextMenuIconProps {
6
+ type: GoAIconType;
7
+ testId?: string;
8
+ title?: string;
9
+ onClick?: () => void;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ interface ContextMenuTextProps {
14
+ type?: GoAIconType;
15
+ testId?: string;
16
+ onClick?: () => void;
17
+ }
18
+
19
+ export const GoAContextMenuIcon: FC<ContextMenuIconProps> = (props) => {
20
+ return (
21
+ <GoAIconButton
22
+ icon={props.type}
23
+ onClick={props.onClick}
24
+ title={props.title}
25
+ testId={props.testId}
26
+ size="small"
27
+ disabled={props.disabled}
28
+ />
29
+ );
30
+ };
31
+
32
+ export const GoAContextMenuText: FC<ContextMenuTextProps> = (props) => {
33
+ return <GoAButton type="tertiary" onClick={props.onClick} testId={props.testId} size="compact" />;
34
+ };
35
+
36
+ export const GoAContextMenu = styled.div`
37
+ display: flex;
38
+ align-items: center;
39
+ background-color: #fff;
40
+ gap: 0.25rem;
41
+
42
+ > .goa-icon-button {
43
+ cursor: pointer;
44
+ border-radius: 0.25rem;
45
+ padding: 0.25rem;
46
+ }
47
+ > .goa-icon-button + .goa-icon-button {
48
+ margin-left: 0rem;
49
+ }
50
+ `;
@@ -0,0 +1,131 @@
1
+ import React, { useContext, useEffect } from 'react';
2
+ import { GoAFileUploadInput, GoAFormItem, GoACircularProgress, GoAModal } from '@abgov/react-components-new';
3
+ import { WithClassname, ControlProps } from '@jsonforms/core';
4
+
5
+ import styled from 'styled-components';
6
+ import { JsonFormContext } from '../../Context';
7
+
8
+ import { GoAContextMenu, GoAContextMenuIcon } from './ContextMenu';
9
+
10
+ type FileUploaderLayoutRendererProps = ControlProps & WithClassname;
11
+
12
+ export const FileUploader = ({ data, path, handleChange, uischema, ...props }: FileUploaderLayoutRendererProps) => {
13
+ const enumerators = useContext(JsonFormContext);
14
+ const uploadTriggerFunction = enumerators.functions.get('upload-file');
15
+ const uploadTrigger = uploadTriggerFunction && uploadTriggerFunction();
16
+ const downloadTriggerFunction = enumerators.functions.get('download-file');
17
+ const downloadTrigger = downloadTriggerFunction && downloadTriggerFunction();
18
+ const deleteTriggerFunction = enumerators.functions.get('delete-file');
19
+ const deleteTrigger = deleteTriggerFunction && deleteTriggerFunction();
20
+ const fileListValue = enumerators.data.get('file-list');
21
+ // eslint-disable-next-line
22
+ const fileList = fileListValue && (fileListValue() as Record<string, any>);
23
+ const { required, label, i18nKeyPrefix } = props;
24
+
25
+ const propertyId = i18nKeyPrefix as string;
26
+
27
+ const variant = uischema?.options?.variant || 'button';
28
+
29
+ function uploadFile(file: File) {
30
+ if (uploadTrigger) {
31
+ handleChange(propertyId, ['Loading', Array.isArray(data) ? data[1] : data, file?.name]);
32
+ uploadTrigger(file, propertyId);
33
+ }
34
+ }
35
+ function downloadFile(file: File) {
36
+ if (downloadTrigger) {
37
+ downloadTrigger(file, propertyId);
38
+ }
39
+ }
40
+ function deleteFile(file: File) {
41
+ if (deleteTrigger) {
42
+ deleteTrigger(file, propertyId);
43
+ }
44
+ }
45
+
46
+ useEffect(() => {
47
+ // UseEffect is required because not having it causes a react update error, but
48
+ // it doesn't function correctly within jsonforms unless there is a minor delay here
49
+ const delayedFunction = () => {
50
+ if (fileList && Array.isArray(data) && data[1] !== fileList[propertyId]?.urn) {
51
+ handleChange(propertyId, fileList && fileList[propertyId]?.urn);
52
+ }
53
+ };
54
+
55
+ const timeoutId = setTimeout(delayedFunction, 1);
56
+ return () => clearTimeout(timeoutId);
57
+ }, [data, handleChange, fileList, propertyId]);
58
+
59
+ return (
60
+ <FileUploaderStyle id="file-upload" className="FileUploader">
61
+ {required ? (
62
+ <GoAFormItem label={label} requirement="required"></GoAFormItem>
63
+ ) : (
64
+ <div className="label">{props.label}</div>
65
+ )}
66
+
67
+ <div className="file-upload">
68
+ <GoAFileUploadInput variant={variant} onSelectFile={uploadFile} />
69
+ </div>
70
+ <div>
71
+ {Array.isArray(data) && data[0] === 'Loading' ? (
72
+ <GoAModal open={Array.isArray(data) && data[0] === 'Loading'}>
73
+ <div className="align-center">
74
+ <GoACircularProgress visible={true} message={`Uploading ${data[2]}`} size="large" />
75
+ </div>
76
+ </GoAModal>
77
+ ) : (
78
+ <div>
79
+ {fileList && fileList[props.i18nKeyPrefix as string] && (
80
+ <AttachmentBorder>
81
+ <div>{fileList && fileList[props.i18nKeyPrefix as string].filename}</div>
82
+ <GoAContextMenu>
83
+ <GoAContextMenuIcon
84
+ testId="download-icon"
85
+ title="Download"
86
+ type="download"
87
+ onClick={() => downloadFile(fileList && fileList[props.i18nKeyPrefix as string])}
88
+ />
89
+ <GoAContextMenuIcon
90
+ data-testid="delete-icon"
91
+ title="Delete"
92
+ type="trash"
93
+ onClick={() => deleteFile(fileList && fileList[props.i18nKeyPrefix as string])}
94
+ />
95
+ </GoAContextMenu>
96
+ </AttachmentBorder>
97
+ )}
98
+ </div>
99
+ )}
100
+ </div>
101
+ </FileUploaderStyle>
102
+ );
103
+ };
104
+
105
+ const AttachmentBorder = styled.div`
106
+ display: flex;
107
+ flex-direction: row;
108
+ border: 1px solid #dcdcdc;
109
+ border-radius: 0.25rem;
110
+ padding: 0.5rem;
111
+ width: fit-content;
112
+ margin-top: 5px;
113
+ `;
114
+
115
+ const FileUploaderStyle = styled.div`
116
+ .label {
117
+ display: block;
118
+ font-weight: var(--goa-font-weight-bold);
119
+ color: var(--goa-color-text-default);
120
+ font-size: var(--goa-font-size-4);
121
+ padding: 0 0 0.5rem 0;
122
+ }
123
+
124
+ .align-center {
125
+ text-align-last: center;
126
+ }
127
+
128
+ .file-upload {
129
+ margin-bottom: 0.5rem;
130
+ }
131
+ `;
@@ -0,0 +1,3 @@
1
+ import { rankWith, RankedTester, schemaTypeIs, and, formatIs } from '@jsonforms/core';
2
+
3
+ export const FileUploaderTester: RankedTester = rankWith(3, and(schemaTypeIs('string'), formatIs('file-urn')));
@@ -0,0 +1,2 @@
1
+ export * from './FileUploaderControl';
2
+ export * from './FileUploaderTester';
@@ -0,0 +1,10 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const ReviewItem = styled.div`
4
+ display: flex;
5
+ width: 100%;
6
+ border: 1px solid grey;
7
+ border-radius: 5px;
8
+ margin: 5px;
9
+ padding: 10px;
10
+ `;