From 02cb689b6df8d83e6de41d93ffd02cc8c24d58f6 Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sat, 5 Mar 2022 22:48:19 +1300 Subject: [PATCH 01/14] Basic form and dialog setup --- frontend/package.json | 6 +- .../CreatePostDialog.stories.tsx | 35 ++++ .../CreatePostDialog/CreatePostForm/index.tsx | 35 ++++ .../dialog/CreatePostDialog/index.tsx | 34 ++++ .../components/dialog/DialogHeader/index.tsx | 32 +++ .../components/form/CategorySelect/index.tsx | 30 +++ .../components/form/SubmitButton/index.tsx | 10 + .../src/components/form/TextField/index.tsx | 32 +++ frontend/src/theme/darkTheme.ts | 1 + frontend/src/utils/schema/createPostSchema.ts | 13 ++ frontend/yarn.lock | 187 +++++++++++++++++- 11 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx create mode 100644 frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx create mode 100644 frontend/src/components/dialog/CreatePostDialog/index.tsx create mode 100644 frontend/src/components/dialog/DialogHeader/index.tsx create mode 100644 frontend/src/components/form/CategorySelect/index.tsx create mode 100644 frontend/src/components/form/SubmitButton/index.tsx create mode 100644 frontend/src/components/form/TextField/index.tsx create mode 100644 frontend/src/utils/schema/createPostSchema.ts diff --git a/frontend/package.json b/frontend/package.json index 54393bd..99ef6c6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,8 @@ "dependencies": { "@emotion/react": "^11.8.1", "@emotion/styled": "^11.8.1", + "@hookform/resolvers": "^2.8.8", + "@mui/lab": "^5.0.0-alpha.71", "@mui/material": "^5.4.4", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", @@ -18,11 +20,13 @@ "react": "^17.0.2", "react-dialog-async": "^1.0.8", "react-dom": "^17.0.2", + "react-hook-form": "^7.27.1", "react-router-dom": "6", "react-scripts": "5.0.0", "react-useanimations": "^2.0.8", "typescript": "^4.4.2", - "web-vitals": "^2.1.0" + "web-vitals": "^2.1.0", + "yup": "^0.32.11" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx new file mode 100644 index 0000000..a86b3f3 --- /dev/null +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx @@ -0,0 +1,35 @@ +import { Button, IconButton } from '@mui/material'; +import { Meta, Story } from '@storybook/react'; +import { DialogProvider, useDialog } from 'react-dialog-async'; +import { BrowserRouter } from 'react-router-dom'; + +import CreatePostDialog, { CreatePostDialogProps } from '.'; + +import SearchIcon from '../../icons/SearchIcon'; + +export default { + component: CreatePostDialog, + title: 'Components/CreatePostDialog', + parameters: { layout: 'centered' }, + decorators: [ + (Story) => ( + + + + ), + ], +} as Meta; + +interface CreatePostDialogStoryProps extends CreatePostDialogProps {} + +const Template: Story = (args) => { + const createPostDialog = useDialog(CreatePostDialog); + + return ( + + ); +}; + +export const Default = Template.bind({}); diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx new file mode 100644 index 0000000..6cdc5a6 --- /dev/null +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx @@ -0,0 +1,35 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { Stack } from '@mui/material'; +import { FormProvider, useForm } from 'react-hook-form'; + +import { + createPostFormFields, + createPostFormSchema, +} from '../../../../utils/schema/createPostSchema'; +import { SubmitButton } from '../../../form/SubmitButton'; +import TextField from '../../../form/TextField'; +import SendIcon from '../../../icons/SendIcon'; + +interface CreatePostFormProps { + handleSubmit: (data: createPostFormFields) => void | Promise; +} + +export const CreatePostForm = ({ handleSubmit }: CreatePostFormProps) => { + const methods = useForm({ + resolver: yupResolver(createPostFormSchema), + }); + + return ( + +
+ + + + }> + Create Post + + +
+
+ ); +}; diff --git a/frontend/src/components/dialog/CreatePostDialog/index.tsx b/frontend/src/components/dialog/CreatePostDialog/index.tsx new file mode 100644 index 0000000..cbccee5 --- /dev/null +++ b/frontend/src/components/dialog/CreatePostDialog/index.tsx @@ -0,0 +1,34 @@ +import { Dialog, DialogContent, Grow } from '@mui/material'; +import { AsyncDialogProps } from 'react-dialog-async'; + +import { DialogHeader } from '../DialogHeader'; +import { CreatePostForm } from './CreatePostForm'; + +export interface CreatePostDialogProps {} + +const CreatePostDialog = ({ open, handleClose }: AsyncDialogProps) => ( + handleClose()} + fullWidth + maxWidth='xs' + > + handleClose()}>Create Post + + + new Promise((res) => + window.setTimeout(() => { + console.log(data); + res(); + }, 1000) + ) + } + /> + + +); + +export default CreatePostDialog; diff --git a/frontend/src/components/dialog/DialogHeader/index.tsx b/frontend/src/components/dialog/DialogHeader/index.tsx new file mode 100644 index 0000000..68b92d7 --- /dev/null +++ b/frontend/src/components/dialog/DialogHeader/index.tsx @@ -0,0 +1,32 @@ +import { DialogTitle, DialogTitleProps, IconButton } from '@mui/material'; +import { PropsWithChildren } from 'react'; + +import ExitIcon from '../../icons/ExitIcon'; + +interface DialogHeaderProps extends DialogTitleProps { + onClose: () => void; +} + +export const DialogHeader = ({ + children, + onClose, + ...other +}: PropsWithChildren) => ( + + {children} + {onClose ? ( + + + + ) : null} + +); diff --git a/frontend/src/components/form/CategorySelect/index.tsx b/frontend/src/components/form/CategorySelect/index.tsx new file mode 100644 index 0000000..2570a5e --- /dev/null +++ b/frontend/src/components/form/CategorySelect/index.tsx @@ -0,0 +1,30 @@ +import { Autocomplete, TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +interface CategorySelectProps { + name: string; + label?: string; + placeholder?: string; +} + +export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps) => { + const { control } = useFormContext(); + + return ( + ( + ( + + )} + /> + )} + /> + ); +}; diff --git a/frontend/src/components/form/SubmitButton/index.tsx b/frontend/src/components/form/SubmitButton/index.tsx new file mode 100644 index 0000000..9713386 --- /dev/null +++ b/frontend/src/components/form/SubmitButton/index.tsx @@ -0,0 +1,10 @@ +import { LoadingButtonProps, LoadingButton } from '@mui/lab'; +import { useFormContext } from 'react-hook-form'; + +export const SubmitButton = ({ loading, ...other }: Omit) => { + const { + formState: { isSubmitting }, + } = useFormContext(); + + return ; +}; diff --git a/frontend/src/components/form/TextField/index.tsx b/frontend/src/components/form/TextField/index.tsx new file mode 100644 index 0000000..267bd6a --- /dev/null +++ b/frontend/src/components/form/TextField/index.tsx @@ -0,0 +1,32 @@ +import { FormHelperText, TextField as MuiTextField, Stack, TextFieldProps } from '@mui/material'; +import React, { PropsWithChildren } from 'react'; +import { useFormContext } from 'react-hook-form'; + +const TextField = ({ + disabled, + name, + type, + ...rest +}: PropsWithChildren) => { + const { + register, + formState: { errors, isSubmitting }, + } = useFormContext(); + + const error = errors[name]; + + return ( + + + {error?.message} + + ); +}; + +export default TextField; diff --git a/frontend/src/theme/darkTheme.ts b/frontend/src/theme/darkTheme.ts index 38add2f..a9f5941 100644 --- a/frontend/src/theme/darkTheme.ts +++ b/frontend/src/theme/darkTheme.ts @@ -14,6 +14,7 @@ const darkTheme = createTheme(baseTheme, { light: '#FFEE53', main: '#FFBC11', dark: '#C78C00', + contrastText: '#271933', }, error: { light: '#EC636C', diff --git a/frontend/src/utils/schema/createPostSchema.ts b/frontend/src/utils/schema/createPostSchema.ts new file mode 100644 index 0000000..a36b634 --- /dev/null +++ b/frontend/src/utils/schema/createPostSchema.ts @@ -0,0 +1,13 @@ +import { array, object, SchemaOf, string } from 'yup'; + +export interface createPostFormFields { + content: string; + link?: string; + categories: string[]; +} + +export const createPostFormSchema: SchemaOf = object().shape({ + content: string().required('A post must have content'), + link: string().url('Please enter a valid url'), + categories: array().of(string().required()), +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 7d3642f..2835164 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1564,7 +1564,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.17.2 resolution: "@babel/runtime@npm:7.17.2" dependencies: @@ -1752,6 +1752,69 @@ __metadata: languageName: node linkType: hard +"@date-io/core@npm:^2.13.1": + version: 2.13.1 + resolution: "@date-io/core@npm:2.13.1" + checksum: e998454687af579f3172d2b376ea4efedb47ca13ffac3899608c72b59aadf68d461bb07e1a3d67b90c554b42d3b4ddd8d23be5c9b60281d785f740736fa34fb9 + languageName: node + linkType: hard + +"@date-io/date-fns@npm:^2.13.1": + version: 2.13.1 + resolution: "@date-io/date-fns@npm:2.13.1" + dependencies: + "@date-io/core": ^2.13.1 + peerDependencies: + date-fns: ^2.0.0 + peerDependenciesMeta: + date-fns: + optional: true + checksum: 753e37e30537be02adf83583be102d30d5b00d09d8639f8ac26f5ee167d5a52ae156fb0b190466c66ce78680e74a46fb9ae890d12a8f4338c771207d21a0f3c3 + languageName: node + linkType: hard + +"@date-io/dayjs@npm:^2.13.1": + version: 2.13.1 + resolution: "@date-io/dayjs@npm:2.13.1" + dependencies: + "@date-io/core": ^2.13.1 + peerDependencies: + dayjs: ^1.8.17 + peerDependenciesMeta: + dayjs: + optional: true + checksum: c4f264bcae32eede7062627202abb52bf8f07102e7108f8477a9af18bf869d3904c8818d0b80146ee6eacbeeace7fa6842d534da4af9c998f86c00f30866ab11 + languageName: node + linkType: hard + +"@date-io/luxon@npm:^2.13.1": + version: 2.13.1 + resolution: "@date-io/luxon@npm:2.13.1" + dependencies: + "@date-io/core": ^2.13.1 + peerDependencies: + luxon: ^1.21.3 || ^2.x + peerDependenciesMeta: + luxon: + optional: true + checksum: 359b0548d1de022759bb7f763ad0a8c9dfde7da35714ddc101d903088cf714e4117481c1fbd256d1893750e7bec615de6dcc108599e4e4d6597a1a98a67bcfa1 + languageName: node + linkType: hard + +"@date-io/moment@npm:^2.13.1": + version: 2.13.1 + resolution: "@date-io/moment@npm:2.13.1" + dependencies: + "@date-io/core": ^2.13.1 + peerDependencies: + moment: ^2.24.0 + peerDependenciesMeta: + moment: + optional: true + checksum: 815d8fe6c600f0b77844cd591de7517c6d1feaf5d9331657ccc0dc5d096954e276d41e7947881eefbf796fb5da333ddb708f3c8851197d49968b1077e2cdeed3 + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:^0.5.3": version: 0.5.6 resolution: "@discoveryjs/json-ext@npm:0.5.6" @@ -2562,6 +2625,15 @@ __metadata: languageName: node linkType: hard +"@hookform/resolvers@npm:^2.8.8": + version: 2.8.8 + resolution: "@hookform/resolvers@npm:2.8.8" + peerDependencies: + react-hook-form: ^7.0.0 + checksum: 3de8b00ba3d085cd29b8d9d41bb17f5e31a483e46dafef98a37d898fbf5f3d19a74729e1cda8d15d7513486b5886ea12fbef0c1aa4ed009e3cf3c804cbe325a2 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.9.2": version: 0.9.5 resolution: "@humanwhocodes/config-array@npm:0.9.5" @@ -2947,6 +3019,47 @@ __metadata: languageName: node linkType: hard +"@mui/lab@npm:^5.0.0-alpha.71": + version: 5.0.0-alpha.71 + resolution: "@mui/lab@npm:5.0.0-alpha.71" + dependencies: + "@babel/runtime": ^7.17.2 + "@date-io/date-fns": ^2.13.1 + "@date-io/dayjs": ^2.13.1 + "@date-io/luxon": ^2.13.1 + "@date-io/moment": ^2.13.1 + "@mui/base": 5.0.0-alpha.70 + "@mui/system": ^5.4.4 + "@mui/utils": ^5.4.4 + clsx: ^1.1.1 + prop-types: ^15.7.2 + react-is: ^17.0.2 + react-transition-group: ^4.4.2 + rifm: ^0.12.1 + peerDependencies: + "@mui/material": ^5.0.0 + "@types/react": ^16.8.6 || ^17.0.0 + date-fns: ^2.25.0 + dayjs: ^1.10.7 + luxon: ^1.28.0 || ^2.0.0 + moment: ^2.29.1 + react: ^17.0.0 + react-dom: ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + checksum: 2d5f0117e95504bdfcaea9a1c84dda9f82fb60c983d02217c309f2098f5f645dabb1aebf5b228eb952ee60b7d3bf0b8eea0b393099ffe8a9d177370975cf42b6 + languageName: node + linkType: hard + "@mui/material@npm:^5.4.4": version: 5.4.4 resolution: "@mui/material@npm:5.4.4" @@ -5411,6 +5524,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.14.175": + version: 4.14.179 + resolution: "@types/lodash@npm:4.14.179" + checksum: 71faa0c8071732c2b7f0bd092850d3cea96fc7912055d57d819cf2ab399a64150e4190d8a4ea35a0905662ddc118be9d2abd55891d8047c085acf98608156149 + languageName: node + linkType: hard + "@types/long@npm:^4.0.1": version: 4.0.1 resolution: "@types/long@npm:4.0.1" @@ -11018,7 +11138,9 @@ __metadata: dependencies: "@emotion/react": ^11.8.1 "@emotion/styled": ^11.8.1 + "@hookform/resolvers": ^2.8.8 "@mdx-js/react": ^1.6.22 + "@mui/lab": ^5.0.0-alpha.71 "@mui/material": ^5.4.4 "@openapitools/openapi-generator-cli": ^2.4.26 "@storybook/addon-actions": ^6.4.19 @@ -11047,6 +11169,7 @@ __metadata: react: ^17.0.2 react-dialog-async: ^1.0.8 react-dom: ^17.0.2 + react-hook-form: ^7.27.1 react-router-dom: 6 react-scripts: 5.0.0 react-useanimations: ^2.0.8 @@ -11054,6 +11177,7 @@ __metadata: typescript: ^4.4.2 web-vitals: ^2.1.0 webpack: ^5.70.0 + yup: ^0.32.11 languageName: unknown linkType: soft @@ -14033,6 +14157,13 @@ __metadata: languageName: node linkType: hard +"lodash-es@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash-es@npm:4.17.21" + checksum: 05cbffad6e2adbb331a4e16fbd826e7faee403a1a04873b82b42c0f22090f280839f85b95393f487c1303c8a3d2a010048bf06151a6cbe03eee4d388fb0a12d2 + languageName: node + linkType: hard + "lodash.camelcase@npm:^4.3.0": version: 4.3.0 resolution: "lodash.camelcase@npm:4.3.0" @@ -14808,6 +14939,13 @@ __metadata: languageName: node linkType: hard +"nanoclone@npm:^0.2.1": + version: 0.2.1 + resolution: "nanoclone@npm:0.2.1" + checksum: 96b2954e22f70561f41e20d69856266c65583c2a441dae108f1dc71b716785d2c8038dac5f1d5e92b117aed3825f526b53139e2e5d6e6db8a77cfa35b3b8bf40 + languageName: node + linkType: hard + "nanoid@npm:^3.1.23, nanoid@npm:^3.3.1": version: 3.3.1 resolution: "nanoid@npm:3.3.1" @@ -16939,6 +17077,13 @@ __metadata: languageName: node linkType: hard +"property-expr@npm:^2.0.4": + version: 2.0.5 + resolution: "property-expr@npm:2.0.5" + checksum: 4ebe82ce45aaf1527e96e2ab84d75d25217167ec3ff6378cf83a84fb4abc746e7c65768a79d275881602ae82f168f9a6dfaa7f5e331d0fcc83d692770bcce5f1 + languageName: node + linkType: hard + "property-information@npm:^5.0.0, property-information@npm:^5.3.0": version: 5.6.0 resolution: "property-information@npm:5.6.0" @@ -17351,6 +17496,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.27.1": + version: 7.27.1 + resolution: "react-hook-form@npm:7.27.1" + peerDependencies: + react: ^16.8.0 || ^17 + checksum: 99af1d47d34e74bff723e5e987f7e663ed93f11f32fa91f90802ad783105015b3df17d3024fc48dfb7f227a0a4a43473a56dfe950e7c0904c0532f4e29cb5a9e + languageName: node + linkType: hard + "react-inspector@npm:^5.1.0": version: 5.1.1 resolution: "react-inspector@npm:5.1.1" @@ -18080,6 +18234,15 @@ __metadata: languageName: node linkType: hard +"rifm@npm:^0.12.1": + version: 0.12.1 + resolution: "rifm@npm:0.12.1" + peerDependencies: + react: ">=16.8" + checksum: 7f11621b6a14885bdee0b49c21174627272a5f683f74aaf8444570dae319f86de38d2af55c17e08af1ebb837bf956894971cceadeaca890e4c9cd1d46f02a385 + languageName: node + linkType: hard + "rimraf@npm:^2.2.8, rimraf@npm:^2.5.4, rimraf@npm:^2.6.3": version: 2.7.1 resolution: "rimraf@npm:2.7.1" @@ -19817,6 +19980,13 @@ __metadata: languageName: node linkType: hard +"toposort@npm:^2.0.2": + version: 2.0.2 + resolution: "toposort@npm:2.0.2" + checksum: d64c74b570391c9432873f48e231b439ee56bc49f7cb9780b505cfdf5cb832f808d0bae072515d93834dd6bceca5bb34448b5b4b408335e4d4716eaf68195dcb + languageName: node + linkType: hard + "tough-cookie@npm:^4.0.0": version: 4.0.0 resolution: "tough-cookie@npm:4.0.0" @@ -21536,6 +21706,21 @@ __metadata: languageName: node linkType: hard +"yup@npm:^0.32.11": + version: 0.32.11 + resolution: "yup@npm:0.32.11" + dependencies: + "@babel/runtime": ^7.15.4 + "@types/lodash": ^4.14.175 + lodash: ^4.17.21 + lodash-es: ^4.17.21 + nanoclone: ^0.2.1 + property-expr: ^2.0.4 + toposort: ^2.0.2 + checksum: 43a16786b47cc910fed4891cebdd89df6d6e31702e9462e8f969c73eac88551ce750732608012201ea6b93802c8847cb0aa27b5d57370640f4ecf30f9f97d4b0 + languageName: node + linkType: hard + "zwitch@npm:^1.0.0": version: 1.0.5 resolution: "zwitch@npm:1.0.5" From 5a3b05810a8adcf73fd78ba94e4fd7b970cb4169 Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sat, 5 Mar 2022 23:26:09 +1300 Subject: [PATCH 02/14] Add category selection --- .../CreatePostDialog/CreatePostForm/index.tsx | 21 +++++- .../components/form/CategorySelect/index.tsx | 70 ++++++++++++++----- frontend/src/components/icons/HelpIcon.tsx | 23 ++++++ frontend/src/utils/schema/createPostSchema.ts | 9 ++- 4 files changed, 102 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/icons/HelpIcon.tsx diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx index 6cdc5a6..42fa5d9 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx @@ -2,12 +2,16 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { Stack } from '@mui/material'; import { FormProvider, useForm } from 'react-hook-form'; +import Tooltip from '../../../Tooltip'; + import { createPostFormFields, createPostFormSchema, } from '../../../../utils/schema/createPostSchema'; +import { CategorySelect } from '../../../form/CategorySelect'; import { SubmitButton } from '../../../form/SubmitButton'; import TextField from '../../../form/TextField'; +import HelpIcon from '../../../icons/HelpIcon'; import SendIcon from '../../../icons/SendIcon'; interface CreatePostFormProps { @@ -23,8 +27,21 @@ export const CreatePostForm = ({ handleSubmit }: CreatePostFormProps) => {
- - + + + + + ), + }} + /> + }> Create Post diff --git a/frontend/src/components/form/CategorySelect/index.tsx b/frontend/src/components/form/CategorySelect/index.tsx index 2570a5e..d5742c1 100644 --- a/frontend/src/components/form/CategorySelect/index.tsx +++ b/frontend/src/components/form/CategorySelect/index.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, TextField } from '@mui/material'; +import { Autocomplete, Box, Chip, FormHelperText, TextField } from '@mui/material'; import { Controller, useFormContext } from 'react-hook-form'; interface CategorySelectProps { @@ -8,23 +8,59 @@ interface CategorySelectProps { } export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps) => { - const { control } = useFormContext(); + const { + control, + formState: { errors, isSubmitting }, + } = useFormContext(); + + const error = errors[name]; return ( - ( - ( - - )} - /> - )} - /> + + ( + onChange(v)} + value={value || []} + {...field} + options={categories} + getOptionLabel={(option) => option.text} + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + )) + } + defaultValue={[]} + renderInput={(params) => ( + + )} + /> + )} + /> + {error && {error?.message}} + ); }; + +const categories = [ + { key: 'ai', text: 'AI' }, + { key: 'css', text: 'CSS' }, + { key: 'html', text: 'HTML' }, +]; diff --git a/frontend/src/components/icons/HelpIcon.tsx b/frontend/src/components/icons/HelpIcon.tsx new file mode 100644 index 0000000..2c35603 --- /dev/null +++ b/frontend/src/components/icons/HelpIcon.tsx @@ -0,0 +1,23 @@ +import { createSvgIcon } from '@mui/material'; + +const HelpIcon = createSvgIcon( + + + + + , + 'Help' +); + +export default HelpIcon; diff --git a/frontend/src/utils/schema/createPostSchema.ts b/frontend/src/utils/schema/createPostSchema.ts index a36b634..27687df 100644 --- a/frontend/src/utils/schema/createPostSchema.ts +++ b/frontend/src/utils/schema/createPostSchema.ts @@ -3,11 +3,16 @@ import { array, object, SchemaOf, string } from 'yup'; export interface createPostFormFields { content: string; link?: string; - categories: string[]; + categories: { key: string; text: string }[]; } export const createPostFormSchema: SchemaOf = object().shape({ content: string().required('A post must have content'), link: string().url('Please enter a valid url'), - categories: array().of(string().required()), + categories: array().of( + object().shape({ + key: string().required(), + text: string().required(), + }) + ), }); From 61127bec58a5d8d80a80d709a314acb06ae1d4dc Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sat, 5 Mar 2022 23:35:31 +1300 Subject: [PATCH 03/14] Minor form improvements --- .../dialog/CreatePostDialog/CreatePostForm/index.tsx | 11 ++++++++++- .../src/components/dialog/CreatePostDialog/index.tsx | 2 +- frontend/src/components/icons/Icons.stories.tsx | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx index 42fa5d9..dbf19a7 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx @@ -27,12 +27,21 @@ export const CreatePostForm = ({ handleSubmit }: CreatePostFormProps) => { - + diff --git a/frontend/src/components/dialog/CreatePostDialog/index.tsx b/frontend/src/components/dialog/CreatePostDialog/index.tsx index cbccee5..58e5e2d 100644 --- a/frontend/src/components/dialog/CreatePostDialog/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/index.tsx @@ -13,7 +13,7 @@ const CreatePostDialog = ({ open, handleClose }: AsyncDialogProps handleClose()} fullWidth - maxWidth='xs' + maxWidth='sm' > handleClose()}>Create Post diff --git a/frontend/src/components/icons/Icons.stories.tsx b/frontend/src/components/icons/Icons.stories.tsx index 3bb2e55..a67ebcd 100644 --- a/frontend/src/components/icons/Icons.stories.tsx +++ b/frontend/src/components/icons/Icons.stories.tsx @@ -8,6 +8,7 @@ import FacebookIcon from './FacebookIcon'; import GithubIcon from './GithubIcon'; import GlobeIcon from './GlobeIcon'; import GoogleIcon from './GoogleIcon'; +import HelpIcon from './HelpIcon'; import ListIcon from './ListIcon'; import PlusIcon from './PlusIcon'; import SearchIcon from './SearchIcon'; @@ -48,6 +49,7 @@ export const Default: Story = (args) => ( + @@ -79,3 +81,5 @@ export const Github: ComponentStory = (args) => = (args) => ; export const Google: ComponentStory = (args) => ; + +export const Help: ComponentStory = (args) => ; From 388f36c62565ef1c202ba303c03496ca71b12716 Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sun, 6 Mar 2022 10:47:05 +1300 Subject: [PATCH 04/14] Improve post form --- frontend/package.json | 1 + .../CreatePostDialog.stories.tsx | 11 ++- .../CreatePostDialog/CreatePostForm/index.tsx | 4 +- .../dialog/CreatePostDialog/index.tsx | 51 +++++++------- .../components/form/CategorySelect/index.tsx | 67 +++++++++++++++++-- frontend/src/utils/queryKeys.ts | 3 + frontend/src/utils/schema/createPostSchema.ts | 19 +++--- frontend/yarn.lock | 10 +++ 8 files changed, 126 insertions(+), 40 deletions(-) create mode 100644 frontend/src/utils/queryKeys.ts diff --git a/frontend/package.json b/frontend/package.json index 99ef6c6..6902c75 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "react-router-dom": "6", "react-scripts": "5.0.0", "react-useanimations": "^2.0.8", + "swr": "^1.2.2", "typescript": "^4.4.2", "web-vitals": "^2.1.0", "yup": "^0.32.11" diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx index a86b3f3..732e2f8 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx @@ -1,9 +1,10 @@ -import { Button, IconButton } from '@mui/material'; +import { Button, Container, IconButton } from '@mui/material'; import { Meta, Story } from '@storybook/react'; import { DialogProvider, useDialog } from 'react-dialog-async'; import { BrowserRouter } from 'react-router-dom'; import CreatePostDialog, { CreatePostDialogProps } from '.'; +import { CreatePostForm } from './CreatePostForm'; import SearchIcon from '../../icons/SearchIcon'; @@ -33,3 +34,11 @@ const Template: Story = (args) => { }; export const Default = Template.bind({}); + +const FormTemplate: Story = (args) => ( + + {}} /> + +); + +export const FormOnly = FormTemplate.bind({ parameters: { layout: 'fullscreen' } }); diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx index dbf19a7..9ca6f9d 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx @@ -16,11 +16,13 @@ import SendIcon from '../../../icons/SendIcon'; interface CreatePostFormProps { handleSubmit: (data: createPostFormFields) => void | Promise; + initialValues?: Partial; } -export const CreatePostForm = ({ handleSubmit }: CreatePostFormProps) => { +export const CreatePostForm = ({ handleSubmit, initialValues }: CreatePostFormProps) => { const methods = useForm({ resolver: yupResolver(createPostFormSchema), + defaultValues: initialValues, }); return ( diff --git a/frontend/src/components/dialog/CreatePostDialog/index.tsx b/frontend/src/components/dialog/CreatePostDialog/index.tsx index 58e5e2d..36f42a3 100644 --- a/frontend/src/components/dialog/CreatePostDialog/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/index.tsx @@ -4,31 +4,34 @@ import { AsyncDialogProps } from 'react-dialog-async'; import { DialogHeader } from '../DialogHeader'; import { CreatePostForm } from './CreatePostForm'; +import { createPostFormFields } from '../../../utils/schema/createPostSchema'; + export interface CreatePostDialogProps {} -const CreatePostDialog = ({ open, handleClose }: AsyncDialogProps) => ( - handleClose()} - fullWidth - maxWidth='sm' - > - handleClose()}>Create Post - - - new Promise((res) => - window.setTimeout(() => { - console.log(data); - res(); - }, 1000) - ) - } - /> - - -); +const CreatePostDialog = ({ open, handleClose }: AsyncDialogProps) => { + const handleSubmit = async (data: createPostFormFields) => + new Promise((res) => + window.setTimeout(() => { + console.log(data); + res(); + }, 1000) + ); + + return ( + handleClose()} + fullWidth + maxWidth='sm' + > + handleClose()}>Create Post + + + + + ); +}; export default CreatePostDialog; diff --git a/frontend/src/components/form/CategorySelect/index.tsx b/frontend/src/components/form/CategorySelect/index.tsx index d5742c1..3a67b43 100644 --- a/frontend/src/components/form/CategorySelect/index.tsx +++ b/frontend/src/components/form/CategorySelect/index.tsx @@ -1,12 +1,30 @@ -import { Autocomplete, Box, Chip, FormHelperText, TextField } from '@mui/material'; +import { + Autocomplete, + Box, + Chip, + createFilterOptions, + FormHelperText, + TextField, +} from '@mui/material'; import { Controller, useFormContext } from 'react-hook-form'; +import useSWR from 'swr'; -interface CategorySelectProps { +import { queryKey } from '../../../utils/queryKeys'; + +export interface CategorySelectProps { name: string; label?: string; placeholder?: string; } +type Category = { + key: string; + text: string; + new?: boolean; +}; + +const filter = createFilterOptions(); + export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps) => { const { control, @@ -15,19 +33,56 @@ export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps const error = errors[name]; + const query = useSWR(queryKey.CATEGORIES, async () => TEMP_CATEGORIES); + + const handleCreateNewCategory = (value: string) => { + const category: Category = { key: value.toLowerCase(), text: value }; + query.mutate([...(query?.data || []), category], false); + }; + return ( ( - multiple + selectOnFocus + clearOnBlur + handleHomeEndKeys + autoHighlight disabled={isSubmitting} - onChange={(e, v) => onChange(v)} + loading={Boolean(query.error)} + loadingText={'❌ Failed to load categories'} + isOptionEqualToValue={(option, value) => option.key === value.key} + onChange={(e, v, reason, option) => { + if (option?.option.new) { + handleCreateNewCategory(option.option.text); + } + onChange(v); + }} value={value || []} {...field} - options={categories} + filterOptions={(options, params) => { + const filteredOpts = filter(options, params); + + const { inputValue } = params; + // Suggest the creation of a new value + const isExisting = options.some((option) => inputValue.toLowerCase() === option.key); + + if (inputValue !== '' && !isExisting) { + // Add a new option to the list + filteredOpts.push({ + key: inputValue.toLowerCase(), + text: inputValue, + new: true, + }); + } + + return filteredOpts; + }} + options={query.data || []} getOptionLabel={(option) => option.text} renderTags={(value, getTagProps) => value.map((option, index) => ( @@ -59,7 +114,7 @@ export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps ); }; -const categories = [ +const TEMP_CATEGORIES = [ { key: 'ai', text: 'AI' }, { key: 'css', text: 'CSS' }, { key: 'html', text: 'HTML' }, diff --git a/frontend/src/utils/queryKeys.ts b/frontend/src/utils/queryKeys.ts new file mode 100644 index 0000000..cf91680 --- /dev/null +++ b/frontend/src/utils/queryKeys.ts @@ -0,0 +1,3 @@ +export const queryKey = { + CATEGORIES: 'categories', +}; diff --git a/frontend/src/utils/schema/createPostSchema.ts b/frontend/src/utils/schema/createPostSchema.ts index 27687df..10b5ab3 100644 --- a/frontend/src/utils/schema/createPostSchema.ts +++ b/frontend/src/utils/schema/createPostSchema.ts @@ -1,18 +1,21 @@ -import { array, object, SchemaOf, string } from 'yup'; +import { array, boolean, object, SchemaOf, string } from 'yup'; export interface createPostFormFields { content: string; link?: string; - categories: { key: string; text: string }[]; + categories: { key: string; text: string; new?: boolean }[]; } export const createPostFormSchema: SchemaOf = object().shape({ content: string().required('A post must have content'), link: string().url('Please enter a valid url'), - categories: array().of( - object().shape({ - key: string().required(), - text: string().required(), - }) - ), + categories: array() + .of( + object().shape({ + key: string().required().max(20, "Category name can't be more than 20 characters"), + text: string().required(), + new: boolean(), + }) + ) + .max(3, 'You can only select up to 3 categories'), }); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 2835164..d0db73d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -11173,6 +11173,7 @@ __metadata: react-router-dom: 6 react-scripts: 5.0.0 react-useanimations: ^2.0.8 + swr: ^1.2.2 ts-node: ^10.6.0 typescript: ^4.4.2 web-vitals: ^2.1.0 @@ -19594,6 +19595,15 @@ __metadata: languageName: node linkType: hard +"swr@npm:^1.2.2": + version: 1.2.2 + resolution: "swr@npm:1.2.2" + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + checksum: 723747c7c263536d8c9ee8b38039462395ba09a7925508f22c8fc940a1ae72e1f05ef678767b2197ce337e170f4812563281bc4b5f9ad7e4eec895cd7139fb8e + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" From 02becd51241fac5147c240d3bf910d087a16bfa6 Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sun, 6 Mar 2022 11:02:30 +1300 Subject: [PATCH 05/14] Call API on post creation --- frontend/src/api/index.tsx | 3 ++ .../CreatePostDialog.stories.tsx | 25 ++++++++---- .../CreatePostDialog/CreatePostForm/index.tsx | 7 ++-- .../dialog/CreatePostDialog/index.tsx | 39 +++++++++++++------ 4 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 frontend/src/api/index.tsx diff --git a/frontend/src/api/index.tsx b/frontend/src/api/index.tsx new file mode 100644 index 0000000..c18bb57 --- /dev/null +++ b/frontend/src/api/index.tsx @@ -0,0 +1,3 @@ +import { PostsApi } from './client'; + +export const postsApi = new PostsApi(undefined, process.env.REACT_APP_API_URL); diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx index 732e2f8..c3c8172 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostDialog.stories.tsx @@ -1,22 +1,22 @@ -import { Button, Container, IconButton } from '@mui/material'; +import { Button, Container } from '@mui/material'; import { Meta, Story } from '@storybook/react'; +import { SnackbarProvider } from 'notistack'; import { DialogProvider, useDialog } from 'react-dialog-async'; -import { BrowserRouter } from 'react-router-dom'; import CreatePostDialog, { CreatePostDialogProps } from '.'; import { CreatePostForm } from './CreatePostForm'; -import SearchIcon from '../../icons/SearchIcon'; - export default { component: CreatePostDialog, title: 'Components/CreatePostDialog', parameters: { layout: 'centered' }, decorators: [ (Story) => ( - - - + + + + + ), ], } as Meta; @@ -27,13 +27,22 @@ const Template: Story = (args) => { const createPostDialog = useDialog(CreatePostDialog); return ( - ); }; export const Default = Template.bind({}); +export const Editing = Template.bind({}); +Editing.args = { + editMode: true, + initialValues: { + content: + 'Ullamco cupidatat amet sunt aute aute do ipsum nulla proident. Ut laboris consequat culpa Lorem est. Mollit occaecat sunt mollit ipsum non aliquip sit aute tempor laborum aliquip do occaecat aute.', + link: 'https://google.com', + }, +}; const FormTemplate: Story = (args) => ( diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx index 9ca6f9d..568bc71 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx @@ -17,12 +17,13 @@ import SendIcon from '../../../icons/SendIcon'; interface CreatePostFormProps { handleSubmit: (data: createPostFormFields) => void | Promise; initialValues?: Partial; + editMode?: boolean; } -export const CreatePostForm = ({ handleSubmit, initialValues }: CreatePostFormProps) => { +export const CreatePostForm = ({ handleSubmit, initialValues, editMode }: CreatePostFormProps) => { const methods = useForm({ resolver: yupResolver(createPostFormSchema), - defaultValues: initialValues, + defaultValues: { ...initialValues, categories: initialValues?.categories || [] }, }); return ( @@ -54,7 +55,7 @@ export const CreatePostForm = ({ handleSubmit, initialValues }: CreatePostFormPr /> }> - Create Post + {editMode ? 'Save' : 'Create Post'} diff --git a/frontend/src/components/dialog/CreatePostDialog/index.tsx b/frontend/src/components/dialog/CreatePostDialog/index.tsx index 36f42a3..91d04f9 100644 --- a/frontend/src/components/dialog/CreatePostDialog/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/index.tsx @@ -1,21 +1,36 @@ import { Dialog, DialogContent, Grow } from '@mui/material'; +import { useSnackbar } from 'notistack'; import { AsyncDialogProps } from 'react-dialog-async'; import { DialogHeader } from '../DialogHeader'; import { CreatePostForm } from './CreatePostForm'; +import { postsApi } from '../../../api'; +import { CreatePost } from '../../../api/client'; import { createPostFormFields } from '../../../utils/schema/createPostSchema'; -export interface CreatePostDialogProps {} +export interface CreatePostDialogProps { + initialValues?: Partial; + editMode?: boolean; +} -const CreatePostDialog = ({ open, handleClose }: AsyncDialogProps) => { - const handleSubmit = async (data: createPostFormFields) => - new Promise((res) => - window.setTimeout(() => { - console.log(data); - res(); - }, 1000) - ); +const CreatePostDialog = ({ open, handleClose, data }: AsyncDialogProps) => { + const { enqueueSnackbar } = useSnackbar(); + const handleSubmit = async ({ content, link, categories }: createPostFormFields) => { + const request: CreatePost = { + content, + resource: link === '' ? undefined : link, + categories: categories.map(({ text }) => text), + }; + + try { + await postsApi.postsPost({ createPost: request }); + enqueueSnackbar(data.editMode ? 'Post updated' : 'Post created', { variant: 'success' }); + handleClose(); + } catch (error) { + enqueueSnackbar('An error occurred', { variant: 'error' }); + } + }; return ( - handleClose()}>Create Post + handleClose()}> + {data.editMode ? 'Edit Post' : 'Create Post'} + - + ); From ecdde8553370fd6b529753b8f511afde2e56b451 Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sun, 6 Mar 2022 11:11:29 +1300 Subject: [PATCH 06/14] Add save button when editing --- .../CreatePostDialog/CreatePostForm/index.tsx | 8 +++++++- .../components/dialog/CreatePostDialog/index.tsx | 6 +++++- frontend/src/components/icons/Icons.stories.tsx | 4 ++++ frontend/src/components/icons/SaveIcon.tsx | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/icons/SaveIcon.tsx diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx index 568bc71..37c9a19 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx @@ -12,6 +12,7 @@ import { CategorySelect } from '../../../form/CategorySelect'; import { SubmitButton } from '../../../form/SubmitButton'; import TextField from '../../../form/TextField'; import HelpIcon from '../../../icons/HelpIcon'; +import SaveIcon from '../../../icons/SaveIcon'; import SendIcon from '../../../icons/SendIcon'; interface CreatePostFormProps { @@ -54,7 +55,12 @@ export const CreatePostForm = ({ handleSubmit, initialValues, editMode }: Create }} /> - }> + : } + > {editMode ? 'Save' : 'Create Post'}
diff --git a/frontend/src/components/dialog/CreatePostDialog/index.tsx b/frontend/src/components/dialog/CreatePostDialog/index.tsx index 91d04f9..125d17b 100644 --- a/frontend/src/components/dialog/CreatePostDialog/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/index.tsx @@ -45,7 +45,11 @@ const CreatePostDialog = ({ open, handleClose, data }: AsyncDialogProps - + ); diff --git a/frontend/src/components/icons/Icons.stories.tsx b/frontend/src/components/icons/Icons.stories.tsx index a67ebcd..232b001 100644 --- a/frontend/src/components/icons/Icons.stories.tsx +++ b/frontend/src/components/icons/Icons.stories.tsx @@ -11,6 +11,7 @@ import GoogleIcon from './GoogleIcon'; import HelpIcon from './HelpIcon'; import ListIcon from './ListIcon'; import PlusIcon from './PlusIcon'; +import SaveIcon from './SaveIcon'; import SearchIcon from './SearchIcon'; import SendIcon from './SendIcon'; import UserIcon from './UserIcon'; @@ -53,6 +54,7 @@ export const Default: Story = (args) => ( + ); @@ -83,3 +85,5 @@ export const Facebook: ComponentStory = (args) => = (args) => ; export const Help: ComponentStory = (args) => ; + +export const Save: ComponentStory = (args) => ; diff --git a/frontend/src/components/icons/SaveIcon.tsx b/frontend/src/components/icons/SaveIcon.tsx new file mode 100644 index 0000000..4eacd55 --- /dev/null +++ b/frontend/src/components/icons/SaveIcon.tsx @@ -0,0 +1,15 @@ +import { createSvgIcon } from '@mui/material'; + +const SaveIcon = createSvgIcon( + + + , + 'Save' +); + +export default SaveIcon; From 09416c467ac108ef95ea2b46388e8b120da98498 Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sun, 6 Mar 2022 11:15:11 +1300 Subject: [PATCH 07/14] Make creatable an option for category select --- .../dialog/CreatePostDialog/CreatePostForm/index.tsx | 2 +- frontend/src/components/form/CategorySelect/index.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx index 37c9a19..f6e7285 100644 --- a/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/CreatePostForm/index.tsx @@ -54,7 +54,7 @@ export const CreatePostForm = ({ handleSubmit, initialValues, editMode }: Create ), }} /> - + (); -export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps) => { +export const CategorySelect = ({ name, label, placeholder, allowCreate }: CategorySelectProps) => { const { control, formState: { errors, isSubmitting }, @@ -57,7 +58,7 @@ export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps loadingText={'❌ Failed to load categories'} isOptionEqualToValue={(option, value) => option.key === value.key} onChange={(e, v, reason, option) => { - if (option?.option.new) { + if (allowCreate && option?.option.new) { handleCreateNewCategory(option.option.text); } onChange(v); @@ -71,7 +72,7 @@ export const CategorySelect = ({ name, label, placeholder }: CategorySelectProps // Suggest the creation of a new value const isExisting = options.some((option) => inputValue.toLowerCase() === option.key); - if (inputValue !== '' && !isExisting) { + if (allowCreate && inputValue !== '' && !isExisting) { // Add a new option to the list filteredOpts.push({ key: inputValue.toLowerCase(), From 334ce6c8438c9369938380b90c558ab7c5bd401e Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sun, 6 Mar 2022 13:47:58 +1300 Subject: [PATCH 08/14] Add global page --- .../components/Post/PostSkeleton/index.tsx | 44 +++++++++++++ .../dialog/CreatePostDialog/index.tsx | 2 +- .../layout/Page/PageDivider/index.tsx | 9 +++ frontend/src/components/layout/Page/index.tsx | 56 ++++++++++------- .../components/layout/PageHeader/index.tsx | 7 ++- frontend/src/pages/GlobalPage/index.tsx | 63 +++++++++++++++++-- 6 files changed, 150 insertions(+), 31 deletions(-) create mode 100644 frontend/src/components/Post/PostSkeleton/index.tsx create mode 100644 frontend/src/components/layout/Page/PageDivider/index.tsx diff --git a/frontend/src/components/Post/PostSkeleton/index.tsx b/frontend/src/components/Post/PostSkeleton/index.tsx new file mode 100644 index 0000000..c90d172 --- /dev/null +++ b/frontend/src/components/Post/PostSkeleton/index.tsx @@ -0,0 +1,44 @@ +import { CardHeader, Skeleton, Stack, Typography } from '@mui/material'; + +interface PostSkeletonProps {} + +export const PostSkeleton = (props: PostSkeletonProps) => ( + + } + title={ + + + + + + + + + } + subheader={ + + + + + } + /> + + + + + + {/* {data.resource && ( + + )} */} + + + + + +); diff --git a/frontend/src/components/dialog/CreatePostDialog/index.tsx b/frontend/src/components/dialog/CreatePostDialog/index.tsx index 125d17b..6c249f2 100644 --- a/frontend/src/components/dialog/CreatePostDialog/index.tsx +++ b/frontend/src/components/dialog/CreatePostDialog/index.tsx @@ -24,7 +24,7 @@ const CreatePostDialog = ({ open, handleClose, data }: AsyncDialogProps ( + + + +); diff --git a/frontend/src/components/layout/Page/index.tsx b/frontend/src/components/layout/Page/index.tsx index 8ee7a66..571de73 100644 --- a/frontend/src/components/layout/Page/index.tsx +++ b/frontend/src/components/layout/Page/index.tsx @@ -1,29 +1,37 @@ -import { Container, Typography } from "@mui/material"; -import { FC } from "react"; +import { Container, Stack, styled } from '@mui/material'; +import { FC } from 'react'; import { Helmet } from 'react-helmet-async'; -import { PAGE_MARGIN } from "../../../utils/constants"; +import PageHeader, { PageHeaderProps } from '../PageHeader'; -interface PageProps { - title: string; +import { PAGE_MARGIN, SIDEBAR_WIDTH } from '../../../utils/constants'; + +interface PageProps extends PageHeaderProps { + bottomActions?: React.ReactNode; } -export const Page: FC = ({ children, title }) => ( - - - {title} | Lorem Ipsum - - - {title} - - {children} - - ); +export const Page: FC = ({ bottomActions, children, title, ...other }) => ( + + + {title} + + + {children} + {bottomActions && ( + + + + {bottomActions} + + + + )} + +); + +const ActionContainer = styled(Container)(({ theme }) => ({ + position: 'fixed', + bottom: 0, + left: 0, + right: 0, +})); diff --git a/frontend/src/components/layout/PageHeader/index.tsx b/frontend/src/components/layout/PageHeader/index.tsx index 86f11e5..16eb527 100644 --- a/frontend/src/components/layout/PageHeader/index.tsx +++ b/frontend/src/components/layout/PageHeader/index.tsx @@ -14,6 +14,7 @@ import { useNavigate } from 'react-router-dom'; import Tooltip from '../../Tooltip'; import { useAuth } from '../../../hooks/useAuth'; +import { PAGE_MARGIN } from '../../../utils/constants'; import ArrowLeftIcon from '../../icons/ArrowLeftIcon'; export interface PageHeaderProps { @@ -36,7 +37,7 @@ const PageHeader = ({ title, action, backButton = false }: PageHeaderProps) => { {backButton && ( navigate(-1)} data-testid='back-button' > @@ -47,7 +48,7 @@ const PageHeader = ({ title, action, backButton = false }: PageHeaderProps) => { {title} - {user && } + {user && } {action} @@ -59,6 +60,8 @@ const PageHeader = ({ title, action, backButton = false }: PageHeaderProps) => { const AppBar = styled(Box)(({ theme }) => ({ position: 'sticky', + marginLeft: theme.spacing(-PAGE_MARGIN), + marginRight: theme.spacing(-PAGE_MARGIN), top: 0, background: theme.palette.background.default, zIndex: theme.zIndex.appBar, diff --git a/frontend/src/pages/GlobalPage/index.tsx b/frontend/src/pages/GlobalPage/index.tsx index c749ea6..816a27f 100644 --- a/frontend/src/pages/GlobalPage/index.tsx +++ b/frontend/src/pages/GlobalPage/index.tsx @@ -1,12 +1,67 @@ -import { Typography } from '@mui/material'; +import { + Box, + Card, + CardActionArea, + CardContent, + Fab, + IconButton, + Stack, + Typography, +} from '@mui/material'; +import { useDialog } from 'react-dialog-async'; +import CreatePostDialog from '../../components/dialog/CreatePostDialog'; +import PlusIcon from '../../components/icons/PlusIcon'; +import SearchIcon from '../../components/icons/SearchIcon'; +import WriteIcon from '../../components/icons/WriteIcon'; import { Page } from '../../components/layout/Page'; +import { PageDivider } from '../../components/layout/Page/PageDivider'; +import { Post } from '../../components/Post'; +import { PostSkeleton } from '../../components/Post/PostSkeleton'; +import { post1 } from '../../components/Post/testData'; const GlobalPage = () => { - const yup = 0; + const createPostDialog = useDialog(CreatePostDialog); + const handleCreatePost = async () => { + await createPostDialog.show({}); + }; + return ( - - global page + + + + } + bottomActions={ + <> + + + + + + } + > + + + + + + Write a post + + + + + + + + + + + + + ); }; From 09665f0a0985a3b5f98a75494c48e506e8b67079 Mon Sep 17 00:00:00 2001 From: alexn400 Date: Sun, 6 Mar 2022 15:26:10 +1300 Subject: [PATCH 09/14] Create feed page --- frontend/package.json | 1 + frontend/public/index.html | 5 +- frontend/src/components/Loading/index.tsx | 84 +++++++++++++++++++ frontend/src/components/Post/testData.tsx | 4 +- .../components/auth/AuthProvider/index.tsx | 11 +++ .../src/components/feed/EndOfFeed/index.tsx | 16 ++++ frontend/src/components/icons/SadBoxIcon.tsx | 29 +++++++ frontend/src/pages/GlobalPage/index.tsx | 7 +- frontend/src/pages/Layout/index.tsx | 32 ++++--- frontend/src/routes/index.tsx | 14 +++- frontend/yarn.lock | 19 +++++ 11 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/Loading/index.tsx create mode 100644 frontend/src/components/feed/EndOfFeed/index.tsx create mode 100644 frontend/src/components/icons/SadBoxIcon.tsx diff --git a/frontend/package.json b/frontend/package.json index 285e348..002356e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "react-dialog-async": "^1.0.8", "react-dom": "^17.0.2", "react-hook-form": "^7.27.1", + "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "6", "react-scripts": "5.0.0", "react-useanimations": "^2.0.8", diff --git a/frontend/public/index.html b/frontend/public/index.html index aa069f2..6a9f8c2 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -5,10 +5,7 @@ - +