diff --git a/.eslintrc b/.eslintrc index 377e099a..85f69afd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -28,6 +28,8 @@ "react/jsx-filename-extension": "off", "react/jsx-props-no-spreading": "off", "react/forbid-prop-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "import/prefer-default-export": "off", "react/prop-types": [ 2, { @@ -41,7 +43,7 @@ "js": "never", "jsx": "never", "ts": "never", - "tsx": "never", + "tsx": "never" } ], "import/order": [ @@ -76,6 +78,10 @@ { "pattern": "@Contexts/*", "group": "internal" + }, + { + "pattern": "@Config/*", + "group": "internal" } ] } @@ -92,7 +98,8 @@ ["@Pages", "./src/pages"], ["@HOCs", "./src/HOCs"], ["@Images", "./src/images"], - ["@Contexts", "./src/contexts"] + ["@Contexts", "./src/contexts"], + ["@Config", "./src/config"] ], "extensions": [".js", ".jsx", ".json", ".ts", ".tsx"] }, diff --git a/CODEOWNERS b/CODEOWNERS index aa0c662c..070d76ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,3 +3,4 @@ # These owners will be the default owners for everything in the repo. @HKN-UCSD/SoftwareDevs + \ No newline at end of file diff --git a/README.md b/README.md index 7c59b047..5117d63e 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,13 @@ npm run start | **src/components** | Reusable components go here | | **src/pages** | Pages go here - a page is a component that is directly rendered by a Route in our react router | | **src/constants** | Constants go here | -| **src/images** | Contains images used throughout project | -| src/contexts.js | React contexts go here | -| src/index.js | Entry point of app | +| **src/HOCs** | Higher-order components used for frontend RBAC go here | +| **src/services** | Service functions used by React components, either to make an HTTP request to backend or process information or extract data, go here | +| src/storybook | Introduction to storybook and non-component specific stories go here | +| src/images | Contains images used throughout project | +| src/contexts.ts | React contexts go here | +| src/config.ts | Environment variables are gathered into one single config object here | +| src/index.tsx | Entry point of app | | src/serviceWorker.js | Runs separately from the main browser thread, intercepting network requests, caching or retrieving resources from the cache, and delivering push messages | | .env | Environment variables | | firebase.json | Defines Firebase Hosting configuration | diff --git a/config-overrides.js b/config-overrides.js index 7eb24347..99daa082 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -19,5 +19,6 @@ module.exports = override( '@HOCs': path.resolve(__dirname, './src/HOCs'), '@Images': path.resolve(__dirname, './src/images'), '@Contexts': path.resolve(__dirname, './src/contexts'), + '@Config': path.resolve(__dirname, './src/config'), }) ); diff --git a/guides/adding_new_firebase_functions.md b/guides/adding_new_firebase_functions.md deleted file mode 100644 index acceb2db..00000000 --- a/guides/adding_new_firebase_functions.md +++ /dev/null @@ -1,10 +0,0 @@ -# Adding New Firebase Functions - -## Definition - -We define a firebase function as a function that interacts with the backend, which is currently Firebase. - -All firebase functions may be found in the src/services folder. - -For example, to create a function that gets events, you should add the corresponding function in src/services/events.js. -Please make sure to return promise wrapping JS objects/arrays and not snapshots! The user of the firebase functions should not have to know about what you're doing with the snapshots! diff --git a/guides/miscellaneous/date_time_library_to_use.md b/guides/miscellaneous/date_time_library_to_use.md new file mode 100644 index 00000000..be71fd55 --- /dev/null +++ b/guides/miscellaneous/date_time_library_to_use.md @@ -0,0 +1,7 @@ +# Date Time Library To Use + +Use **date-fns** for any of your date time needs. Do not use **moment.js** or any other DateTime library, nor should you directly use the DateTime API offered by JS/TS. + +We used to have moment.js as our main DateTime library, but switched to date-fns for very good reasons that can be found [here](https://github.com/you-dont-need/You-Dont-Need-Momentjs). + +## Use date-fns please! diff --git a/guides/path_alias/add_path_alias.md b/guides/path_alias/add_path_alias.md new file mode 100644 index 00000000..58b234d6 --- /dev/null +++ b/guides/path_alias/add_path_alias.md @@ -0,0 +1,51 @@ +# Add Path Aliases + +Follow the steps below to add your own path alias. + +### 1. Determine the name and the destination to point to for your path alias + +The name of the path alias should be in UpperCamelCase and it should be as short and concise as possible. + +The destination to point to should not coincide with the destination that an already existing path alias points to. We don't want multiple path aliases pointing to the same place. + +### 2. Navigate to ./tsconfig.paths.json and add your path alias + +Go to the `paths` property of `compilerOptions` and add a new key-value pair + +- `"@YourPathAlias": ["DestinationPathToReplace"]` + +Example: `"@Pages": ["src/pages"]` + +### 3. Navigate to ./config-overrides.js and add your path alias + +Go to the line starting with `module.exports` (should be at the end of the file) then look at `addWebpackAlias`. Add your path alias as a parameter to `addWebpackAlias` + +- `'@YourPathAlias': path.resolve(__dirname, 'DestinationPathToReplace')` + +Example: `'@Images': path.resolve(__dirname, './src/images')` + +### 4. Navigate to ./eslintrc and add your path alias + +Go to the `rules` property of the config JSON object, then to the `import/order` property of `rules`. Then, go to the `pathGroups` property and add your path alias + +``` + { + "pattern": "@YourPathAlias/*", + "group": "internal" + } +``` + +Example: + +``` +{ + "pattern": "@Constants/*", + "group": "internal" +} +``` + +### 5. Restart VSCode to start using the path alias + +Now you can use the path aliases whenever you need to, making import statements much cleaner and easier to read! + +**Please make sure that your path alias string and destination path string all match up across all three files mentioned** diff --git a/guides/path_alias/path_alias.md b/guides/path_alias/path_alias.md new file mode 100644 index 00000000..e6c19d12 --- /dev/null +++ b/guides/path_alias/path_alias.md @@ -0,0 +1,41 @@ +# Path Aliases + +## Definition + +A path alias is a string mapped to a specific file/folder path that can be either relative or absolute. + +A path alias still represents the actual file/folder path and has the option of reaching to the subfolder(s) and subfile(s) of the path it replaces (if that path leads to a folder). + +## Rationale + +Writing the filepaths for import statements can be a big pain. An example of this is: + +- The folder **src/pages/EventDetailsPage/components/EventDetails** has an index.tsx file. This index file imports components Tag, Card and Button from **src/components**. +- This means that to import Tag, Card and Button from **src/components** to index.tsx without using path aliases, a developer would have to write an import statement like this: `import { Tag, Card, Button } from '../../../../components';`. +- This is rather cumbersome to write and unclean to look at, which will multiply even more if there are multiple files/folders to import from. + +To greatly alleviate this issue, path aliases were onboarded. They help make the import paths a lot less burdensome to write and to look at. + +## Available Path Aliases + +Currently in this codebase, there are 8 path aliases in total. They are: + +- `@Pages` - Points to **src/pages** +- `@Constants` - Points to **src/constants** +- `@SharedComponents` - Points to **src/components** +- `@Services` - Points to **src/services** +- `@HOCs` - Points to **src/HOCs** +- `@Images` - Points to **src/images** +- `@Contexts` - Points to **src/contexts** +- `@Config` - Points to **src/config** + +## Usage + +`import from '';` + +To use the example elaborated earlier in the _Rationale_ paragraph, using path aliases would give: +`import { Tag, Card, Button } from '@SharedComponents';` + +## Add More Path Aliases + +Refer to this [doc](./add_path_alias.md). diff --git a/package-lock.json b/package-lock.json index 3550ad01..67c4db29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4503,6 +4503,25 @@ "@types/react": "*" } }, + "@types/react-router": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz", + "integrity": "sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg==", + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.5.tgz", + "integrity": "sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw==", + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/react-syntax-highlighter": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz", @@ -4647,6 +4666,11 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" }, + "@types/yup": { + "version": "0.29.6", + "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.6.tgz", + "integrity": "sha512-YPDo5L5uHyxQ4UkyJST+33stD8Z6IT9fvmKyaPAGxkZ6q19foEi6sQGkmqBvzSyRPdstFEeJiS2rKuTn8rfO5g==" + }, "@typescript-eslint/eslint-plugin": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.9.1.tgz", @@ -6570,15 +6594,6 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "bl": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", @@ -11236,12 +11251,6 @@ } } }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true - }, "filefy": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz", @@ -11958,6 +11967,11 @@ "resolved": "https://registry.npmjs.org/formik-material-ui/-/formik-material-ui-2.0.1.tgz", "integrity": "sha512-kX+SJuFj9AdjLk7sfZczDfJIK8W/MnNtHWFZ182LkhN4743IFho7+VYycd2QL9qnEEv7vdZhbEm1ts+wp3nPEg==" }, + "formik-material-ui-lab": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/formik-material-ui-lab/-/formik-material-ui-lab-0.0.5.tgz", + "integrity": "sha512-beLimoEpmszMKekk8WiZG71r0sGtaH/vICSq3piaZEA9iPcJgaLC+wtF+AAe0TSTUtzm8SuzE12hbpX+XuWeew==" + }, "formik-material-ui-pickers": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/formik-material-ui-pickers/-/formik-material-ui-pickers-0.0.8.tgz", @@ -12649,19 +12663,23 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", - "bundled": true + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -12669,11 +12687,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12681,57 +12701,69 @@ }, "chownr": { "version": "1.1.2", - "bundled": true + "resolved": false, + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" }, "code-point-at": { "version": "1.1.0", - "bundled": true + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", - "bundled": true + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", - "bundled": true + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "debug": { "version": "3.2.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { "ms": "^2.1.1" } }, "deep-extend": { "version": "0.6.0", - "bundled": true + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "delegates": { "version": "1.0.0", - "bundled": true + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "detect-libc": { "version": "1.0.3", - "bundled": true + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "fs-minipass": { "version": "1.2.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", "requires": { "minipass": "^2.2.1" } }, "fs.realpath": { "version": "1.0.0", - "bundled": true + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -12745,7 +12777,8 @@ }, "glob": { "version": "7.1.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -12757,25 +12790,29 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "requires": { "minimatch": "^3.0.4" } }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -12783,37 +12820,44 @@ }, "inherits": { "version": "2.0.4", - "bundled": true + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", - "bundled": true + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.0", - "bundled": true + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "minipass": { "version": "2.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -12821,31 +12865,36 @@ }, "minizlib": { "version": "1.2.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "requires": { "minipass": "^2.2.1" } }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" }, "dependencies": { "minimist": { "version": "0.0.8", - "bundled": true + "resolved": false, + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, "ms": { "version": "2.1.2", - "bundled": true + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "needle": { "version": "2.4.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -12854,7 +12903,8 @@ }, "node-pre-gyp": { "version": "0.13.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz", + "integrity": "sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==", "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -12870,7 +12920,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -12878,11 +12929,13 @@ }, "npm-bundled": { "version": "1.0.6", - "bundled": true + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" }, "npm-packlist": { "version": "1.4.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz", + "integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==", "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" @@ -12890,7 +12943,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -12900,30 +12954,36 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", - "bundled": true + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "bundled": true + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", - "bundled": true + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -12931,11 +12991,13 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "process-nextick-args": { "version": "2.0.1", - "bundled": true + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "protobufjs": { "version": "5.0.3", @@ -12950,7 +13012,8 @@ }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -12960,7 +13023,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -12973,38 +13037,46 @@ }, "rimraf": { "version": "2.7.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "requires": { "glob": "^7.1.3" } }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", - "bundled": true + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "1.2.4", - "bundled": true + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { "version": "5.7.1", - "bundled": true + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "set-blocking": { "version": "2.0.0", - "bundled": true + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "signal-exit": { "version": "3.0.2", - "bundled": true + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -13013,25 +13085,29 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "tar": { "version": "4.4.10", - "bundled": true, + "resolved": false, + "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -13044,22 +13120,26 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", - "bundled": true + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.3", - "bundled": true + "resolved": false, + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" } } }, @@ -15030,7 +15110,6 @@ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true, "requires": { - "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -18086,6 +18165,22 @@ "prepend-http": "^1.0.0", "query-string": "^4.1.0", "sort-keys": "^1.0.0" + }, + "dependencies": { + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + } } }, "npm-run-path": { @@ -20123,12 +20218,13 @@ "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" }, "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.2.tgz", + "integrity": "sha512-BMmDaUiLDFU1hlM38jTFcRt7HYiGP/zt1sRzrIWm5zpeEuO1rkbPS0ELI3uehoLuuhHDCS8u8lhFN3fEN4JzPQ==", "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" } }, "querystring": { @@ -23619,6 +23715,11 @@ } } }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -23752,9 +23853,9 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" }, "string-argv": { "version": "0.3.1", @@ -25533,7 +25634,6 @@ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true, "requires": { - "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -26121,7 +26221,6 @@ "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true, "requires": { - "bindings": "^1.5.0", "nan": "^2.12.1" } }, diff --git a/package.json b/package.json index 99474d8f..265cf55c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "@storybook/react": "^6.0.13", "@types/react": "^16.9.46", "@types/react-dom": "^16.9.8", + "@types/react-router": "^5.1.8", + "@types/react-router-dom": "^5.1.5", + "@types/yup": "^0.29.6", "@typescript-eslint/parser": "^3.9.1", "classnames": "^2.2.6", "customize-cra": "^0.9.1", @@ -25,10 +28,12 @@ "firebase-tools": "^7.16.2", "formik": "^2.1.4", "formik-material-ui": "^2.0.0-beta.1", + "formik-material-ui-lab": "0.0.5", "formik-material-ui-pickers": "0.0.8", "material-table": "1.68.0", "material-ui": "^0.20.2", "prop-types": "^15.7.2", + "query-string": "^6.13.2", "react": "^16.13.1", "react-app-rewire-hot-loader": "^2.0.1", "react-app-rewired": "^2.1.6", @@ -48,11 +53,11 @@ "build": "react-app-rewired build", "test": "react-app-rewired test --passWithNoTests", "cypress": "npx cypress run", - "lint": "eslint --fix './src/**/*.{js,ts,tsx}'", + "lint": "eslint --fix ./src/**/*.{js,ts,tsx}", "storybook": "start-storybook -p 6006 -s public", "build-storybook": "build-storybook -s public", "precodegen": "rimraf src/api/*", - "codegen": "npx openapi-generator generate -i http://dev.api.hknucsd.com/api/docs/json -g typescript-fetch --additional-properties=typescriptThreePlus=true -o src/api/" + "codegen": "npx openapi-generator generate -i https://api.hknucsd.com/api/docs/json -g typescript-fetch --additional-properties=typescriptThreePlus=true -o src/services/api" }, "eslintConfig": { "extends": "react-app" diff --git a/src/components/NavBar/index.js b/src/components/NavBar/index.js index 988b7841..b9626fb4 100644 --- a/src/components/NavBar/index.js +++ b/src/components/NavBar/index.js @@ -40,8 +40,8 @@ const INITIAL_STATES = { }; class NavBar extends React.Component { - constructor(props) { - super(props); + constructor() { + super(); this.state = { ...INITIAL_STATES }; } diff --git a/src/components/Tags/index.js b/src/components/Tags/index.js index 6a18b8b5..c55171c2 100644 --- a/src/components/Tags/index.js +++ b/src/components/Tags/index.js @@ -11,7 +11,11 @@ function Tags({ classes, tags }) { return (
{tags.map(tag => ( - + ))}
); diff --git a/src/components/autocomplete/OfficerNameAutocomplete/index.tsx b/src/components/autocomplete/OfficerNameAutocomplete/index.tsx new file mode 100644 index 00000000..a12ecc2f --- /dev/null +++ b/src/components/autocomplete/OfficerNameAutocomplete/index.tsx @@ -0,0 +1,52 @@ +import React, { useState, useEffect } from 'react'; + +import { BaseAutocomplete } from '../base'; + +import { getMultipleUsers, getUserNames } from '@Services/UserService'; + +export type OfficerNameData = { + id: number; + firstName: string; + lastName: string; +}; + +type OfficerAutocompleteProp = { + name: string; + label: string; + fullWidth?: boolean; +}; + +export const OfficerNameAutocomplete = (props: OfficerAutocompleteProp) => { + const { name, label, fullWidth = false } = props; + const [officerNames, setOfficerNames] = useState([]); + + useEffect(() => { + getMultipleUsers({ + officers: true, + names: true, + }).then(officerNameArr => { + setOfficerNames(getUserNames(officerNameArr)); + }); + }, []); + + return ( + `${option.firstName} ${option.lastName}`} + getOptionSelected={(option, value) => + option.id === value.id && + option.firstName === value.firstName && + option.lastName === value.lastName + } + fullWidth={fullWidth} + /> + ); +}; + +OfficerNameAutocomplete.defaultProps = { + fullWidth: false, +}; diff --git a/src/components/autocomplete/base/index.tsx b/src/components/autocomplete/base/index.tsx new file mode 100644 index 00000000..092da822 --- /dev/null +++ b/src/components/autocomplete/base/index.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Field } from 'formik'; +import { Autocomplete } from 'formik-material-ui-lab'; +import { TextField } from '@material-ui/core'; +import { AutocompleteRenderInputParams } from '@material-ui/lab/Autocomplete'; + +type BaseAutoCompleteProp = { + name: string; + label: string; + options: Array; + getOptionSelected?: (option: any, value?: any) => any; + getOptionLabel?: (option: any) => any; + multiple?: boolean; + filterSelectedOptions?: boolean; + fullWidth?: boolean; +}; + +export const BaseAutocomplete = ({ + name, + label, + options, + getOptionSelected, + getOptionLabel, + multiple = false, + filterSelectedOptions = false, + fullWidth = false, +}: BaseAutoCompleteProp) => { + return ( + ( + + )} + /> + ); +}; + +BaseAutocomplete.defaultProps = { + multiple: false, + filterSelectedOptions: false, + fullWidth: false, + getOptionLabel: () => { + return null; + }, + getOptionSelected: () => { + return null; + }, +}; diff --git a/src/components/autocomplete/index.ts b/src/components/autocomplete/index.ts new file mode 100644 index 00000000..c48aa431 --- /dev/null +++ b/src/components/autocomplete/index.ts @@ -0,0 +1 @@ +export { OfficerNameAutocomplete } from './OfficerNameAutocomplete'; diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx index 645ed093..0a0aecc7 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -4,7 +4,7 @@ import { ButtonProps as MuiButtonProps, } from '@material-ui/core'; -export interface ButtonProps { +export interface ButtonProps extends MuiButtonProps { primary?: boolean; secondary?: boolean; positive?: boolean; diff --git a/src/components/cards/Card.stories.tsx b/src/components/cards/Card.stories.tsx new file mode 100644 index 00000000..dae9ac19 --- /dev/null +++ b/src/components/cards/Card.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react'; + +import { Card, CardProps } from './Card'; + +export default { + title: 'Cards/Card', + component: Card, +} as Meta; + +const Template: Story = args => { + const { children, title } = args; + return {children}; +}; + +export const SampleCard = Template.bind({}); +SampleCard.args = { + children:

Put stuff here!

, +}; + +export const SampleCardWithTitle = Template.bind({}); +SampleCardWithTitle.args = { + children:

This is the body

, + title: 'Title', +}; diff --git a/src/components/cards/Card.tsx b/src/components/cards/Card.tsx new file mode 100644 index 00000000..34200d25 --- /dev/null +++ b/src/components/cards/Card.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { + Card as MuiCard, + CardHeader as MuiCardHeader, + CardContent as MuiCardContent, +} from '@material-ui/core'; + +export interface CardProps { + title?: string; + children: JSX.Element; + className?: string; +} + +export function Card({ children, title, className }: CardProps): JSX.Element { + return ( + + {title ? : null} + {children} + + ); +} diff --git a/src/components/cards/CardWithVerticalTabs.stories.tsx b/src/components/cards/CardWithVerticalTabs.stories.tsx new file mode 100644 index 00000000..f7d711cf --- /dev/null +++ b/src/components/cards/CardWithVerticalTabs.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react'; + +import { + CardWithVerticalTabs, + CardWithVerticalTabsProps, +} from './CardWithVerticalTabs'; + +export default { + title: 'Cards/Card with Vertical Tabs', + component: CardWithVerticalTabs, +} as Meta; + +const Template: Story = args => { + const { items } = args; + return ; +}; + +export const SampleCardWithVerticalTabs = Template.bind({}); +SampleCardWithVerticalTabs.args = { + items: [ + { title: 'Title 1', element:

Title 1

}, + { title: 'Title 2', element:

Title 2

}, + { title: 'Title 3', element:

Title 3

}, + ], +}; diff --git a/src/components/cards/CardWithVerticalTabs.tsx b/src/components/cards/CardWithVerticalTabs.tsx new file mode 100644 index 00000000..d95a6999 --- /dev/null +++ b/src/components/cards/CardWithVerticalTabs.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { + Grid, + Tabs as MuiTabs, + Tab as MuiTab, + makeStyles, + Theme, +} from '@material-ui/core'; + +import { Card } from './Card'; + +export interface CardWithVerticalTabsProps { + items: { title: string; element: JSX.Element }[]; +} + +const useStyles = makeStyles((theme: Theme) => ({ + tabs: { borderRight: `1px solid ${theme.palette.divider}` }, +})); + +export function CardWithVerticalTabs({ items }: CardWithVerticalTabsProps) { + const [index, setIndex] = useState(0); + const classes = useStyles(); + + const handleChange = (_: React.ChangeEvent, newValue: number) => { + setIndex(newValue); + }; + + const tabElements: JSX.Element[] = items.map(item => ( + + )); + + return ( + + + + + {tabElements} + + + {items[index].element} + + + ); +} diff --git a/src/components/cards/index.ts b/src/components/cards/index.ts new file mode 100644 index 00000000..1e15afd1 --- /dev/null +++ b/src/components/cards/index.ts @@ -0,0 +1 @@ +export { Card } from './Card'; diff --git a/src/components/dropdowns/AffiliateDropdownField/index.js b/src/components/dropdowns/AffiliateDropdownField/index.js index 746ab889..c819148a 100644 --- a/src/components/dropdowns/AffiliateDropdownField/index.js +++ b/src/components/dropdowns/AffiliateDropdownField/index.js @@ -1,19 +1,15 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; import GenericDropdownField from '../base'; -import styles from './styles'; - import HKN_AFFILIATIONS from '@Constants/hknAffiliations'; const AffiliateDropdownField = props => { - const { classes, name, label, ...otherProps } = props; + const { name, label, ...otherProps } = props; return ( ({ - root: { - minWidth: '381px', - }, -}); - -export default styles; diff --git a/src/components/dropdowns/EventStatusDropdownField/index.tsx b/src/components/dropdowns/EventStatusDropdownField/index.tsx new file mode 100644 index 00000000..b493af3d --- /dev/null +++ b/src/components/dropdowns/EventStatusDropdownField/index.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import GenericDropdownField from '@SharedComponents/dropdowns/base'; +import { EventStatusEnum } from '@Services/EventService'; + +type EventStatusFieldProp = { + name: string; + label: string; + fullWidth?: boolean; +}; + +const EventStatusDropdownField = (props: EventStatusFieldProp) => { + const { name, label, fullWidth = false } = props; + + return ( + + ); +}; + +EventStatusDropdownField.defaultProps = { + fullWidth: false, +}; + +export default EventStatusDropdownField; diff --git a/src/components/dropdowns/EventTypeDropdownField/index.tsx b/src/components/dropdowns/EventTypeDropdownField/index.tsx new file mode 100644 index 00000000..2878125e --- /dev/null +++ b/src/components/dropdowns/EventTypeDropdownField/index.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import GenericDropdownField from '@SharedComponents/dropdowns/base'; +import { EventTypeEnum } from '@Services/EventService'; + +type EventTypeFieldProp = { + name: string; + label: string; + fullWidth?: boolean; +}; + +const EventTypeDropdownField = (props: EventTypeFieldProp) => { + const { name, label, fullWidth = false } = props; + + return ( + + ); +}; + +EventTypeDropdownField.defaultProps = { + fullWidth: false, +}; + +export default EventTypeDropdownField; diff --git a/src/components/dropdowns/MajorDropdownField/index.js b/src/components/dropdowns/MajorDropdownField/index.js index fc7e21ba..925c665f 100644 --- a/src/components/dropdowns/MajorDropdownField/index.js +++ b/src/components/dropdowns/MajorDropdownField/index.js @@ -1,11 +1,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; import GenericDropdownField from '../base'; -import styles from './styles'; - import ELIGIBLE_MAJORS from '@Constants/eligibleMajors'; const createFullMajorTitle = (department, major) => { @@ -37,10 +34,8 @@ const MajorDropdownField = props => { return ( @@ -57,4 +52,4 @@ MajorDropdownField.defaultProps = { includeOthers: false, }; -export default withStyles(styles)(MajorDropdownField); +export default MajorDropdownField; diff --git a/src/components/dropdowns/MajorDropdownField/styles.js b/src/components/dropdowns/MajorDropdownField/styles.js deleted file mode 100644 index ece329ba..00000000 --- a/src/components/dropdowns/MajorDropdownField/styles.js +++ /dev/null @@ -1,7 +0,0 @@ -const styles = () => ({ - root: { - minWidth: '273.333px', - }, -}); - -export default styles; diff --git a/src/components/dropdowns/YearDropdownField/index.js b/src/components/dropdowns/YearDropdownField/index.js index 8dce3db3..78c7761a 100644 --- a/src/components/dropdowns/YearDropdownField/index.js +++ b/src/components/dropdowns/YearDropdownField/index.js @@ -1,12 +1,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { getYear } from 'date-fns'; -import { withStyles } from '@material-ui/core/styles'; import GenericDropdownField from '../base'; -import styles from './styles'; - const yearDropdownChoices = (minYear, maxYear) => { const yearChoices = []; @@ -18,11 +15,10 @@ const yearDropdownChoices = (minYear, maxYear) => { }; const YearDropdownField = props => { - const { classes, name, label, minYear, maxYear, ...otherProps } = props; + const { name, label, minYear, maxYear, ...otherProps } = props; return ( ({ - root: { - minWidth: '124.667px', - }, -}); - -export default styles; diff --git a/src/components/dropdowns/base/index.js b/src/components/dropdowns/base/index.js index 618b992c..36cf9c32 100644 --- a/src/components/dropdowns/base/index.js +++ b/src/components/dropdowns/base/index.js @@ -5,7 +5,15 @@ import { TextField as FormikTextField } from 'formik-material-ui'; import { Field } from 'formik'; const GenericDropdownField = props => { - const { name, label, selections, readOnly, ...otherProps } = props; + const { + name, + label, + selections, + readOnly, + capitalizeLabel, + ...otherProps + } = props; + const finalSelections = selections; if (readOnly) { const readOnlyProps = { @@ -26,9 +34,14 @@ const GenericDropdownField = props => { label={label} {...otherProps} > - {selections.map(selection => ( + {finalSelections.map(selection => ( - {selection} + {capitalizeLabel + ? selection + .toString() + .charAt(0) + .toUpperCase() + selection.toString().slice(1) + : selection} ))}
@@ -39,11 +52,15 @@ GenericDropdownField.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.string.isRequired, readOnly: PropTypes.bool, + capitalizeLabel: PropTypes.bool, + fullWidth: PropTypes.bool, selections: PropTypes.arrayOf(PropTypes.node).isRequired, }; GenericDropdownField.defaultProps = { readOnly: false, + capitalizeLabel: false, + fullWidth: false, }; export default GenericDropdownField; diff --git a/src/components/dropdowns/index.js b/src/components/dropdowns/index.js index 4377ae4f..1ac64071 100644 --- a/src/components/dropdowns/index.js +++ b/src/components/dropdowns/index.js @@ -1,5 +1,13 @@ import MajorDropdownField from './MajorDropdownField'; import YearDropdownField from './YearDropdownField'; import AffiliateDropdownField from './AffiliateDropdownField'; +import EventStatusDropdownField from './EventStatusDropdownField'; +import EventTypeDropdownField from './EventTypeDropdownField'; -export { MajorDropdownField, YearDropdownField, AffiliateDropdownField }; +export { + MajorDropdownField, + YearDropdownField, + AffiliateDropdownField, + EventStatusDropdownField, + EventTypeDropdownField, +}; diff --git a/src/components/formSections/PersonalInfoSection.js b/src/components/formSections/PersonalInfoSection.js index 4b7513d0..09840d9a 100644 --- a/src/components/formSections/PersonalInfoSection.js +++ b/src/components/formSections/PersonalInfoSection.js @@ -12,7 +12,7 @@ const getPersonalInfoSection = params => { readOnly = params.readOnly; } - const { firstName, lastName, major, gradYear } = params || {}; + const { firstName, lastName, major, graduationYear } = params || {}; return { title: 'Personal Info', @@ -50,7 +50,7 @@ const getPersonalInfoSection = params => { {readOnly ? ( - + ) : ( )} diff --git a/src/components/index.js b/src/components/index.js index 15b52c88..6b986481 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -3,26 +3,34 @@ import { MajorDropdownField, YearDropdownField, AffiliateDropdownField, + EventStatusDropdownField, + EventTypeDropdownField, } from './dropdowns'; import FormLayout from './FormLayout'; import InputField from './InputField'; import Loading from './Loading'; -import { ButtonWithConfirmationModal, ButtonWithAlertModal } from './modals'; +import { Card } from './cards'; +import { PublicPageLayout } from './layouts'; import NavBar from './NavBar'; import Table from './Table'; import Tags from './Tags'; +export { OfficerNameAutocomplete } from './autocomplete'; +export { ButtonWithConfirmationModal, ButtonWithAlertModal } from './modals'; + export { Button, + Card, MajorDropdownField, YearDropdownField, AffiliateDropdownField, + EventTypeDropdownField, + EventStatusDropdownField, FormLayout, InputField, Loading, - ButtonWithConfirmationModal, - ButtonWithAlertModal, NavBar, Table, Tags, + PublicPageLayout, }; diff --git a/src/components/layouts/PublicPageLayout/PublicPageLayout.stories.tsx b/src/components/layouts/PublicPageLayout/PublicPageLayout.stories.tsx new file mode 100644 index 00000000..ff112517 --- /dev/null +++ b/src/components/layouts/PublicPageLayout/PublicPageLayout.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Card, CardContent } from '@material-ui/core'; +import { Story, Meta } from '@storybook/react'; + +import PublicPageLayout, { PublicPageLayoutProps } from './PublicPageLayout'; + +export default { + title: 'Layouts/PublicPageLayout', + component: PublicPageLayout, +} as Meta; + +const Template: Story = args => { + const { children } = args; + return {children}; +}; + +export const SamplePublicPageLayout = Template.bind({}); +SamplePublicPageLayout.args = { + children: ( + + +

Title

+
+
+ ), +}; diff --git a/src/components/layouts/PublicPageLayout/PublicPageLayout.tsx b/src/components/layouts/PublicPageLayout/PublicPageLayout.tsx new file mode 100644 index 00000000..9fc3ef63 --- /dev/null +++ b/src/components/layouts/PublicPageLayout/PublicPageLayout.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Grid, WithStyles, withStyles } from '@material-ui/core'; + +import styles from './styles'; + +export interface PublicPageLayoutProps extends WithStyles { + children: JSX.Element; +} + +function PublicPageLayout(props: PublicPageLayoutProps): JSX.Element { + const { children, classes } = props; + return ( + + + {children} + + + ); +} + +export default withStyles(styles)(PublicPageLayout); diff --git a/src/components/layouts/PublicPageLayout/index.ts b/src/components/layouts/PublicPageLayout/index.ts new file mode 100644 index 00000000..ac86081e --- /dev/null +++ b/src/components/layouts/PublicPageLayout/index.ts @@ -0,0 +1,3 @@ +import PublicPageLayout from './PublicPageLayout'; + +export default PublicPageLayout; diff --git a/src/components/layouts/PublicPageLayout/styles.ts b/src/components/layouts/PublicPageLayout/styles.ts new file mode 100644 index 00000000..caf8a5b2 --- /dev/null +++ b/src/components/layouts/PublicPageLayout/styles.ts @@ -0,0 +1,17 @@ +import { Theme } from '@material-ui/core'; + +const styles = (theme: Theme) => ({ + root: { + [theme.breakpoints.up('sm')]: { + marginTop: '8vh', + }, + [theme.breakpoints.only('xs')]: { + marginTop: '3vh', + }, + }, + children: { + margin: theme.spacing(2), + }, +}); + +export default styles; diff --git a/src/components/layouts/index.ts b/src/components/layouts/index.ts new file mode 100644 index 00000000..d9a60b11 --- /dev/null +++ b/src/components/layouts/index.ts @@ -0,0 +1,3 @@ +import PublicPageLayout from './PublicPageLayout'; + +export { PublicPageLayout }; diff --git a/src/components/modals/BaseModal.tsx b/src/components/modals/BaseModal.tsx new file mode 100644 index 00000000..13d7975e --- /dev/null +++ b/src/components/modals/BaseModal.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@material-ui/core'; + +interface BaseModalProps { + title: string; + content: string; + open: boolean; + handleClose: () => void; + children: JSX.Element[] | JSX.Element; +} + +export const BaseModal = ({ + title, + content, + open, + handleClose, + children, +}: BaseModalProps) => { + return ( + + {title} + + + {content} + + + {children} + + ); +}; diff --git a/src/components/modals/ButtonWithAlertModal.tsx b/src/components/modals/ButtonWithAlertModal.tsx new file mode 100644 index 00000000..fc6ac1da --- /dev/null +++ b/src/components/modals/ButtonWithAlertModal.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { ButtonProps } from '../buttons/Button'; + +import { ButtonWithModal, ModalTitleContentProps } from './ButtonWithModal'; + +export interface AlertModalProps extends ModalTitleContentProps { + closeButtonProps: ButtonProps; +} + +export interface ButtonWithAlertModalProps extends ButtonProps { + alertModalProps: AlertModalProps; +} + +export const ButtonWithAlertModal = ({ + alertModalProps, + name, + ...otherButtonProps +}: ButtonWithAlertModalProps) => { + const { + closeButtonProps, + ...modalTitleContentProps + }: AlertModalProps = alertModalProps; + + return ( + + ); +}; diff --git a/src/components/modals/ButtonWithConfirmationModal.tsx b/src/components/modals/ButtonWithConfirmationModal.tsx new file mode 100644 index 00000000..40625b4a --- /dev/null +++ b/src/components/modals/ButtonWithConfirmationModal.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { ButtonProps } from '../buttons/Button'; + +import { ButtonWithModal, ModalTitleContentProps } from './ButtonWithModal'; + +export interface ConfirmationModalProps extends ModalTitleContentProps { + confirmButtonProps: ButtonProps; + cancelButtonProps: ButtonProps; +} + +export interface ButtonWithConfirmationModalProps extends ButtonProps { + confirmationModalProps: ConfirmationModalProps; +} + +export const ButtonWithConfirmationModal = ({ + confirmationModalProps, + name, + onClick, + ...otherButtonProps +}: ButtonWithConfirmationModalProps) => { + const { + cancelButtonProps, + confirmButtonProps, + ...modalTitleContentProps + }: ConfirmationModalProps = confirmationModalProps; + + return ( + + ); +}; diff --git a/src/components/modals/ButtonWithModal.tsx b/src/components/modals/ButtonWithModal.tsx new file mode 100644 index 00000000..1d81b13c --- /dev/null +++ b/src/components/modals/ButtonWithModal.tsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; + +import { Button, ButtonProps } from '../buttons/Button'; + +import { ModalProps, ModalWithActionButtons } from './ModalWithActionButtons'; + +export interface ModalTitleContentProps { + title: string; + content: string; +} + +export interface ButtonWithModalProps extends ButtonProps { + modalTitleContentProps: ModalTitleContentProps; + actionButtonPropsList: ButtonProps[]; +} + +export const ButtonWithModal = ({ + modalTitleContentProps, + name, + actionButtonPropsList, + ...otherButtonProps +}: ButtonWithModalProps) => { + const [open, setOpen] = useState(false); + const modalProps: ModalProps = { + ...modalTitleContentProps, + open, + handleClose: () => setOpen(false), + }; + + return ( + <> + + + + + ); +}; diff --git a/src/components/modals/ModalWithActionButtons.tsx b/src/components/modals/ModalWithActionButtons.tsx new file mode 100644 index 00000000..fdb52aca --- /dev/null +++ b/src/components/modals/ModalWithActionButtons.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { Button, ButtonProps } from '../buttons/Button'; + +import { BaseModal } from './BaseModal'; + +export interface ModalProps { + title: string; + content: string; + open: boolean; + handleClose: () => void; +} + +interface ModalWithActionButtonProps { + modalProps: ModalProps; + actionButtonPropsList: ButtonProps[]; +} + +export const ModalWithActionButtons = ({ + modalProps, + actionButtonPropsList, +}: ModalWithActionButtonProps) => { + return ( + + {actionButtonPropsList.map((buttonProps: ButtonProps) => { + const { name, onClick, ...otherProps } = buttonProps; + + const onClickFunction = ( + event: React.MouseEvent + ) => { + if (onClick !== undefined) { + onClick(event); + } + + modalProps.handleClose(); + }; + + return ( + + ); + })} + + ); +}; diff --git a/src/components/modals/README.md b/src/components/modals/README.md new file mode 100644 index 00000000..3b4da6cf --- /dev/null +++ b/src/components/modals/README.md @@ -0,0 +1,8 @@ +The component abstraction level for for modals goes (from lowest to highest level): + +1. BaseModal +2. ModalWithActionButtons +3. ButtonWithModal +4. ButtonWithAlertModal, ButtonWithConfirmationModal + +The (i+1)-th component depends on the i-th component. diff --git a/src/components/modals/base/BaseModal.js b/src/components/modals/base/BaseModal.js deleted file mode 100644 index 6e23b9ac..00000000 --- a/src/components/modals/base/BaseModal.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, -} from '@material-ui/core'; - -const BaseModal = ({ title, contentText, open, handleClose, children }) => { - const closeModalWithCallback = actionButtonFunc => { - handleClose(); - - if (actionButtonFunc != null) { - actionButtonFunc(); - } - }; - - return ( - - {title} - - - {contentText} - - - - {children(actionButtonFunc => closeModalWithCallback(actionButtonFunc))} - - - ); -}; - -BaseModal.propTypes = { - title: PropTypes.string.isRequired, - contentText: PropTypes.string.isRequired, - open: PropTypes.bool.isRequired, - handleClose: PropTypes.func.isRequired, - children: PropTypes.func, -}; - -BaseModal.defaultProps = { - // eslint-disable-next-line @typescript-eslint/no-empty-function - children: () => {}, -}; - -export default BaseModal; diff --git a/src/components/modals/base/ButtonWithModal.js b/src/components/modals/base/ButtonWithModal.js deleted file mode 100644 index cfa39f1a..00000000 --- a/src/components/modals/base/ButtonWithModal.js +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; - -import Button from '../../buttons'; - -import BaseModal from './BaseModal'; - -const ButtonWithModal = props => { - const [open, setOpen] = useState(false); - - const { title, contentText, children, name, ...otherProps } = props; - - return ( - <> - - - setOpen(false)} - > - {children} - - - ); -}; - -ButtonWithModal.propTypes = { - title: PropTypes.string.isRequired, - contentText: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - children: PropTypes.func, -}; - -ButtonWithModal.defaultProps = { - /* eslint-disable-next-line @typescript-eslint/no-empty-function */ - children: () => {}, -}; - -export default ButtonWithModal; diff --git a/src/components/modals/index.js b/src/components/modals/index.js deleted file mode 100644 index 43ae2bc7..00000000 --- a/src/components/modals/index.js +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import Button from '../buttons'; - -import ButtonWithModal from './base/ButtonWithModal'; - -export const ButtonWithConfirmationModal = ({ - title, - contentText, - name, - confirmButtonProps, - cancelButtonProps, - ...otherProps -}) => { - const { name: cancelName, ...otherCancelProps } = cancelButtonProps; - const { - name: confirmName, - onClick: confirmOnClick, - ...otherConfirmProps - } = confirmButtonProps; - - return ( - - {onClickHOF => ( - <> - - - - - )} - - ); -}; - -ButtonWithConfirmationModal.propTypes = { - title: PropTypes.string.isRequired, - contentText: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - confirmButtonProps: PropTypes.shape({ - name: PropTypes.string.isRequired, - onClick: PropTypes.func, - otherConfirmProps: PropTypes.object, - }), - cancelButtonProps: PropTypes.shape({ - name: PropTypes.string.isRequired, - otherCancelProps: PropTypes.object, - }), -}; - -ButtonWithConfirmationModal.defaultProps = { - confirmButtonProps: { - /* eslint-disable-next-line @typescript-eslint/no-empty-function */ - onClick: () => {}, - otherConfirmProps: {}, - }, - cancelButtonProps: { - /* eslint-disable-next-line @typescript-eslint/no-empty-function */ - onClick: () => {}, - otherCancelProps: {}, - }, -}; - -export const ButtonWithAlertModal = ({ - title, - contentText, - name, - closeButtonProps, - ...otherProps -}) => { - const { name: closeName, ...closeOtherProps } = closeButtonProps; - - return ( - - {onClickHOF => ( - <> - - - )} - - ); -}; - -ButtonWithAlertModal.propTypes = { - title: PropTypes.string.isRequired, - contentText: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - closeButtonProps: PropTypes.shape({ - name: PropTypes.string.isRequired, - otherProps: PropTypes.object, - }), -}; - -ButtonWithAlertModal.defaultProps = { - closeButtonProps: { - /* eslint-disable-next-line @typescript-eslint/no-empty-function */ - onClick: () => {}, - otherProps: {}, - }, -}; diff --git a/src/components/modals/index.tsx b/src/components/modals/index.tsx new file mode 100644 index 00000000..6b1c6218 --- /dev/null +++ b/src/components/modals/index.tsx @@ -0,0 +1,2 @@ +export { ButtonWithConfirmationModal } from './ButtonWithConfirmationModal'; +export { ButtonWithAlertModal } from './ButtonWithAlertModal'; diff --git a/src/components/modals/modals.stories.tsx b/src/components/modals/modals.stories.tsx new file mode 100644 index 00000000..4094df29 --- /dev/null +++ b/src/components/modals/modals.stories.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react'; + +import { ButtonProps } from '../buttons/Button'; + +import { ButtonWithModal, ButtonWithModalProps } from './ButtonWithModal'; + +export default { + title: 'Modals/Button With Modal', + component: ButtonWithModal, +} as Meta; + +const Template: Story = args => ( + +); + +const confirmButtonProps: ButtonProps = { + name: 'Confirm', + onClick: () => alert('You just clicked the confirm button!'), + primary: true, + positive: true, +}; + +const cancelButtonProps: ButtonProps = { + name: 'Cancel', + primary: true, + negative: true, +}; + +export const ButtonWithConfirmationModal = Template.bind({}); +ButtonWithConfirmationModal.args = { + modalTitleContentProps: { + title: 'Sample Button With Confirmation Modal', + content: 'Put any text you want here.', + }, + name: 'Click on me!', + actionButtonPropsList: [cancelButtonProps, confirmButtonProps], + primary: true, + positive: true, +}; + +const closeButtonProps: ButtonProps = { + name: 'Close', +}; + +export const ButtonWithAlertModal = Template.bind({}); +ButtonWithAlertModal.args = { + modalTitleContentProps: { + title: 'Sample Button With Alert Modal', + content: 'Put any text you want here.', + }, + name: 'Click on me!', + actionButtonPropsList: [closeButtonProps], + primary: true, + negative: true, +}; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..4677d170 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,23 @@ +type Config = { + apiKey?: string; + authDomain?: string; + databaseURL?: string; + projectID?: string; + storageBucket?: string; + messagingSenderID?: string; + appID?: string; + apiURL?: string; + nodeEnv?: string; +}; + +export const config: Config = { + apiKey: process.env.REACT_APP_API_KEY, + authDomain: process.env.REACT_APP_AUTH_DOMAIN, + databaseURL: process.env.REACT_APP_DATABASE_URL, + projectID: process.env.REACT_APP_PROJECT_ID, + storageBucket: process.env.REACT_APP_STORAGE_BUCKET, + messagingSenderID: process.env.REACT_APP_MESSAGING_SENDER_ID, + appID: process.env.REACT_APP_APP_ID, + apiURL: process.env.REACT_APP_API_URL, + nodeEnv: process.env.NODE_ENV, +}; diff --git a/src/constants/eventStatus.ts b/src/constants/eventStatus.ts new file mode 100644 index 00000000..aaffe7f9 --- /dev/null +++ b/src/constants/eventStatus.ts @@ -0,0 +1,7 @@ +const EVENT_STATUS = { + PENDING: 'pending', + READY: 'ready', + COMPLETE: 'complete', +}; + +export default EVENT_STATUS; diff --git a/src/constants/eventTags.js b/src/constants/eventTags.js index 53ba72e2..66cf7c91 100644 --- a/src/constants/eventTags.js +++ b/src/constants/eventTags.js @@ -1,7 +1,8 @@ const EVENT_TAGS = { - PROFESSIONAL: 'Professional', - TECHNICAL: 'Technical', - SOCIAL: 'Social', + PROFESSIONAL: 'professional', + TECHNICAL: 'technical', + SOCIAL: 'social', + MENTORSHIP: 'mentorship', }; export default EVENT_TAGS; diff --git a/src/constants/routes.js b/src/constants/routes.js index 38e2d343..c450705a 100644 --- a/src/constants/routes.js +++ b/src/constants/routes.js @@ -9,6 +9,7 @@ export const RESUME = '/resume'; export const INDUCTEES = '/inductees'; export const EVENT_EDIT = '/events/:eventId/edit'; export const EVENT_DETAILS = '/events/:id'; +export const EVENT_CREATION = '/events?create=true'; export const TEST = '/test'; export const PROFILE = '/profile/:id'; export const PROFILE_EDIT = '/profile/:id/edit'; diff --git a/src/contexts.js b/src/contexts.js deleted file mode 100644 index 6c007b48..00000000 --- a/src/contexts.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -// we might make more contexts... -// eslint-disable-next-line import/prefer-default-export -export const UserContext = React.createContext(null); diff --git a/src/contexts.ts b/src/contexts.ts new file mode 100644 index 00000000..fa92f3ca --- /dev/null +++ b/src/contexts.ts @@ -0,0 +1,10 @@ +import React from 'react'; + +// we might make more contexts... +// eslint-disable-next-line import/prefer-default-export +export interface UserContextValues { + userId: string; + userRoles: string[]; +} + +export const UserContext = React.createContext(null); diff --git a/src/index.tsx b/src/index.tsx index 894c5e45..8a8c2464 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,18 +6,21 @@ import 'firebase/firestore'; import * as serviceWorker from './serviceWorker'; import App from '@Pages/App'; +import { config } from '@Config'; -const config = { - apiKey: process.env.REACT_APP_API_KEY, - authDomain: process.env.REACT_APP_AUTH_DOMAIN, - databaseURL: process.env.REACT_APP_DATABASE_URL, - projectId: process.env.REACT_APP_PROJECT_ID, - storageBucket: process.env.REACT_APP_STORAGE_BUCKET, - messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, +const appConfig = { + apiKey: config.apiKey, + authDomain: config.authDomain, + databaseURL: config.databaseURL, + projectId: config.projectID, + storageBucket: config.storageBucket, + messagingSenderId: config.messagingSenderID, }; -firebase.initializeApp(config); +firebase.initializeApp(appConfig); +document.body.style.height = '100%'; +document.body.style.margin = '0'; ReactDOM.render(, document.getElementById('root')); serviceWorker.unregister(); diff --git a/src/pages/App/index.js b/src/pages/App/index.js deleted file mode 100644 index 348d2862..00000000 --- a/src/pages/App/index.js +++ /dev/null @@ -1,156 +0,0 @@ -import React from 'react'; -import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; -import * as firebase from 'firebase/app'; -import 'firebase/auth'; -import { hot } from 'react-hot-loader/root'; - -import { - SignInPage, - SignUpPage, - PointsPage, - InducteePointsPage, - ResumePage, - EventsPage, - CalendarPage, - EventEditPage, - EventDetailsPage, - EventSignInPage, - EventRsvpPage, -} from '@Pages'; -import { Loading } from '@SharedComponents'; -import { UserContext } from '@Contexts'; -import * as ROUTES from '@Constants/routes'; -import { getRolesFromClaims } from '@Services/claims'; -import { - InducteeRoutingPermission, - OfficerRoutingPermission, -} from '@HOCs/RoutingPermissions'; - -const INITIAL_STATES = { - userClaims: null, - isLoading: true, -}; - -class App extends React.Component { - constructor(props) { - super(props); - - this.state = { ...INITIAL_STATES }; - } - - componentDidMount() { - firebase.auth().onAuthStateChanged(async user => { - if (user) { - const tokenResult = await user.getIdTokenResult(); - const { claims } = tokenResult; - - this.setState({ - userClaims: { - userId: claims.user_id, - userRoles: getRolesFromClaims(claims), - }, - isLoading: false, - }); - } else { - this.setState({ - userClaims: null, - isLoading: false, - }); - } - }); - } - - setClaims = claims => { - this.setState({ - userClaims: { - userId: claims.user_id, - userRoles: getRolesFromClaims(claims), - }, - }); - }; - - render() { - const { userClaims, isLoading } = this.state; - - if (isLoading) { - return ; - } - - return ( - - - - } - /> - } /> - } - /> - } - /> - InducteeRoutingPermission(EventsPage)(props)} - /> - InducteeRoutingPermission(PointsPage)(props)} - /> - InducteeRoutingPermission(ResumePage)(props)} - /> - - OfficerRoutingPermission(InducteePointsPage)(props) - } - /> - InducteeRoutingPermission(CalendarPage)(props)} - /> - - InducteeRoutingPermission(EventDetailsPage)(props) - } - /> - OfficerRoutingPermission(EventEditPage)(props)} - /> - {/* InducteeRoutingPermission(ProfilePage)(props)} - /> - InducteeRoutingPermission(ProfileEditPage)(props)} - /> */} - } /> - - - - ); - } -} - -export default process.env.NODE_ENV === 'development' ? hot(App) : App; diff --git a/src/pages/App/index.tsx b/src/pages/App/index.tsx new file mode 100644 index 00000000..4aa6ec73 --- /dev/null +++ b/src/pages/App/index.tsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect } from 'react'; +import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; +import firebase from 'firebase/app'; +import 'firebase/auth'; +import { hot } from 'react-hot-loader/root'; + +import { + SignInPage, + SignUpPage, + PointsPage, + InducteePointsPage, + ResumePage, + EventsPage, + CalendarPage, + EventEditPage, + EventDetailsPage, + EventSignInPage, + EventRsvpPage, + QueriedEventPage, +} from '@Pages'; +import { Loading } from '@SharedComponents'; +import { UserContext, UserContextValues } from '@Contexts'; +import * as ROUTES from '@Constants/routes'; +import { getUserRole } from '@Services/UserService'; +import { + InducteeRoutingPermission, + OfficerRoutingPermission, +} from '@HOCs/RoutingPermissions'; +import ApiConfigStore from '@Services/ApiConfigStore'; +import { config } from '@Config'; + +function App(): JSX.Element { + const [userClaims, setUserClaims] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + firebase.auth().onAuthStateChanged(async user => { + if (user) { + const tokenResult = await user.getIdTokenResult(); + const { claims, token } = tokenResult; + + // TODO if there's no change then don't set state to + // save a rerender + ApiConfigStore.setToken(token || ''); + + const id = parseInt(claims.user_id, 10); + const userRole = await getUserRole(id); + + setUserClaims({ + userId: claims.user_id, + userRoles: [userRole.role], + }); + setIsLoading(false); + } else { + setUserClaims(null); + setIsLoading(false); + } + }); + }, []); + + // eslint-disable-next-line camelcase + const setClaims = async ( + // eslint-disable-next-line camelcase + userID: string, + token: string + ): Promise => { + ApiConfigStore.setToken(token); + + const id = parseInt(userID, 10); + const userRole = await getUserRole(id); + + setUserClaims({ + userId: userID, + userRoles: [userRole.role], + }); + }; + + if (isLoading) { + return ; + } + + return ( + + + + } + /> + } /> + } + /> + } + /> + InducteeRoutingPermission(EventsPage)(props)} + /> + InducteeRoutingPermission(PointsPage)(props)} + /> + InducteeRoutingPermission(ResumePage)(props)} + /> + + OfficerRoutingPermission(InducteePointsPage)(props) + } + /> + InducteeRoutingPermission(CalendarPage)(props)} + /> + } + /> + InducteeRoutingPermission(EventDetailsPage)(props)} + /> + OfficerRoutingPermission(EventEditPage)(props)} + /> + {/* InducteeRoutingPermission(ProfilePage)(props)} + /> + InducteeRoutingPermission(ProfileEditPage)(props)} + /> */} + } /> + + + + ); +} + +export default config.nodeEnv === 'development' ? hot(App) : App; diff --git a/src/pages/CalendarPage/components/Calendar/index.js b/src/pages/CalendarPage/components/Calendar/index.js index f1748ce4..8113f188 100644 --- a/src/pages/CalendarPage/components/Calendar/index.js +++ b/src/pages/CalendarPage/components/Calendar/index.js @@ -48,7 +48,7 @@ export default function Calendar({ events, handleEventClick }) { Calendar.propTypes = { events: PropTypes.arrayOf( PropTypes.shape({ - title: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, startDate: PropTypes.string.isRequired, endDate: PropTypes.string.isRequired, }) diff --git a/src/pages/CalendarPage/components/EventCard/index.js b/src/pages/CalendarPage/components/EventCard/index.js index b9f25faf..bbe5996d 100644 --- a/src/pages/CalendarPage/components/EventCard/index.js +++ b/src/pages/CalendarPage/components/EventCard/index.js @@ -1,12 +1,5 @@ import React from 'react'; -import { - Typography, - Box, - Card, - CardHeader, - CardContent, - Button, -} from '@material-ui/core'; +import { Typography, Box, Button } from '@material-ui/core'; import RoomIcon from '@material-ui/icons/Room'; import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; @@ -15,31 +8,30 @@ import { Link } from 'react-router-dom'; import styles from './styles'; +import { Card } from '@SharedComponents'; + function EventCard({ event, classes }) { return ( <> {event && ( - - - - - {format(parseISO(event.startDate), 'PP')} -{' '} - {format(parseISO(event.startDate), 'p')} to{' '} - {format(parseISO(event.endDate), 'p')} - - - - {event.venue} - - - + + + {format(parseISO(event.startDate), 'PP')} -{' '} + {format(parseISO(event.startDate), 'p')} to{' '} + {format(parseISO(event.endDate), 'p')} + + + + {event.location} + + )} @@ -50,10 +42,16 @@ EventCard.propTypes = { event: PropTypes.shape({ startDate: PropTypes.string.isRequired, endDate: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - venue: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - }).isRequired, + name: PropTypes.string.isRequired, + location: PropTypes.string, + id: PropTypes.number.isRequired, + }), +}; + +EventCard.defaultProps = { + event: { + location: '', + }, }; export default withStyles(styles)(EventCard); diff --git a/src/pages/CalendarPage/components/EventList/index.js b/src/pages/CalendarPage/components/EventList/index.js index 38590e6e..71333850 100644 --- a/src/pages/CalendarPage/components/EventList/index.js +++ b/src/pages/CalendarPage/components/EventList/index.js @@ -35,9 +35,10 @@ function EventList({ events, handleEventClick }) { const listEvent = { id: events[i].id, title: events[i].title, + name: events[i].title, startDateString: format(parseISO(events[i].startDate), 'PPPP p'), endDateString: format(parseISO(events[i].endDate), 'PPPP p'), - venue: events[i].venue, + location: events[i].location, startDate: events[i].startDate, endDate: events[i].endDate, }; @@ -64,11 +65,15 @@ EventList.propTypes = { startDate: PropTypes.string.isRequired, endDate: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - venue: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, + location: PropTypes.string, + id: PropTypes.number.isRequired, }) - ).isRequired, + ), handleEventClick: PropTypes.func.isRequired, }; +EventList.defaultProps = { + events: [{ location: '' }], +}; + export default EventList; diff --git a/src/pages/CalendarPage/index.js b/src/pages/CalendarPage/index.js index ce36b7f2..0fdf0072 100644 --- a/src/pages/CalendarPage/index.js +++ b/src/pages/CalendarPage/index.js @@ -1,17 +1,20 @@ import React from 'react'; import { withStyles } from '@material-ui/core/styles'; -import { Grid, Paper, Container, Button } from '@material-ui/core'; +import { Grid, Paper, Container, Button as MuiButton } from '@material-ui/core'; import Calendar from './components/Calendar'; import EventCard from './components/EventCard'; import EventList from './components/EventList'; import styles from './styles'; -import { getAllEvents } from '@Services/events'; +import * as ROUTES from '@Constants/routes'; +import { OfficerRenderPermission } from '@HOCs/RenderPermissions'; +import { getAllEvents } from '@Services/EventService'; +import { Button } from '@SharedComponents'; class CalendarPage extends React.Component { - constructor(props) { - super(props); + constructor() { + super(); this.state = { events: [], selectedEvent: null, @@ -20,45 +23,22 @@ class CalendarPage extends React.Component { } componentDidMount() { - getAllEvents().then(events => { + getAllEvents().then(multipleEventResponse => { + const { events } = multipleEventResponse; const calendarEvents = []; - events.forEach(newEventParam => { - // make a copy of the event - const newEvent = Object.assign(newEventParam); - // convert timestamp object to ISO time string - newEvent.startDate = newEvent.startDate.toDate().toISOString(); - newEvent.endDate = newEvent.endDate.toDate().toISOString(); + events.forEach(event => { + // make a copy of the event + const newEvent = Object.assign(event); + // For EventList newEvent.title = newEvent.name; - newEvent.venue = newEvent.location; + calendarEvents.push(newEvent); }); + this.setState({ events: calendarEvents }); }); - - // const calendarEvents = [ - // { - // id: 'qO8nJ50tCO57hptbxNZa', - // title: 'Enhanced multi-tasking model', - // description: - // 'Pressure someone institution fund account part. Entire couple develop main.\nMeeting sea school me policy beautiful well. Agent ground so majority care born blood.', - // venue: 'Lindsey Rapids', - // startDate: '2020-05-16T01:30:49+00:00', - // endDate: '2020-05-16T02:30:49+00:00', - // }, - // { - // id: 'mRaXeYuSCMmqMOngDl7B', - // title: 'Customizable bottom-line help-desk', - // description: - // 'Reason clear rest the lay. Customer fill change.\nCampaign member we notice include investment. Near they order particularly western life. Reflect bed offer dinner top Mr of her.', - // venue: 'Frazier Station', - // startDate: '2020-03-30T21:04:26+00:00', - // endDate: '2020-03-30T22:04:26+00:00', - // }, - // ]; - - // this.setState({ events: calendarEvents }); } toggleView() { @@ -80,42 +60,59 @@ class CalendarPage extends React.Component { render() { const { selectedEvent, events, view } = this.state; - const { classes } = this.props; + const { classes, history } = this.props; return ( - - - + + + + {OfficerRenderPermission(Button)({ + secondary: true, + positive: true, + children: 'Create Event', + onClick: () => { + history.push(ROUTES.EVENT_CREATION); + }, + })} + + + { + this.toggleView(); + }} + > + {view === 'calendar' ? 'list View' : 'calendar view'} + + - - - {view === 'calendar' ? ( - this.toggleEventClick(event)} - /> - ) : ( - this.toggleEventClick(event)} - /> + + + + + {view === 'calendar' ? ( + this.toggleEventClick(event)} + /> + ) : ( + this.toggleEventClick(event)} + /> + )} + + + + {selectedEvent && ( + + + + + )} - - - {selectedEvent && ( - - - - - )} + ); } diff --git a/src/pages/CalendarPage/styles.js b/src/pages/CalendarPage/styles.js index 99d26ca6..149fbaed 100644 --- a/src/pages/CalendarPage/styles.js +++ b/src/pages/CalendarPage/styles.js @@ -2,6 +2,9 @@ const styles = () => ({ root: { width: '100%', }, + buttons: { + marginBottom: '10px', + }, }); export default styles; diff --git a/src/pages/EventCreationPage/components/EventCreationForm/index.tsx b/src/pages/EventCreationPage/components/EventCreationForm/index.tsx new file mode 100644 index 00000000..e475c5f7 --- /dev/null +++ b/src/pages/EventCreationPage/components/EventCreationForm/index.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import { Formik, Field, Form } from 'formik'; +import { TextField } from 'formik-material-ui'; +import { DateTimePicker } from 'formik-material-ui-pickers'; +import DateFnsUtils from '@date-io/date-fns'; +import { Button, LinearProgress, Grid } from '@material-ui/core'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import { formatISO } from 'date-fns'; + +import schema from './schema'; + +import { + OfficerNameAutocomplete, + EventTypeDropdownField, +} from '@SharedComponents'; +import { OfficerNameData } from '@SharedComponents/autocomplete/OfficerNameAutocomplete'; +import { EventTypeEnum } from '@Services/EventService'; +import { EventRequest } from '@Services/api/models'; + +interface InitialValuesType { + startDate: string; + endDate: string; + name: string; + location: string; + description: string; + fbURL: string; + canvaURL: string; + type: EventTypeEnum | undefined; + hosts: OfficerNameData[]; +} + +const INITIAL_VALUES: InitialValuesType = { + startDate: formatISO(new Date()), + endDate: formatISO(new Date()), + name: '', + type: EventTypeEnum.Social, + hosts: [], + location: '', + description: '', + fbURL: '', + canvaURL: '', +}; + +interface EventCreationFormProps { + handleSubmit: ( + values: EventRequest, + setSubmitting: (_: boolean) => void + ) => void; + handleCancel: () => void; +} + +export const EventCreationForm = (props: EventCreationFormProps) => { + const { handleSubmit, handleCancel } = props; + const urlObjects = [ + { + name: 'fbURL', + label: 'Facebook URL', + }, + { + name: 'canvaURL', + label: 'Canva URL', + }, + ]; + + return ( + + { + handleSubmit(values, setSubmitting); + }} + > + {({ submitForm, isSubmitting }) => ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {urlObjects.map(urlObject => { + const { name, label } = urlObject; + + return ( + + + + ); + })} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {isSubmitting && } + + )} +
+
+ ); +}; diff --git a/src/pages/EventCreationPage/components/EventCreationForm/schema.ts b/src/pages/EventCreationPage/components/EventCreationForm/schema.ts new file mode 100644 index 00000000..1df403e4 --- /dev/null +++ b/src/pages/EventCreationPage/components/EventCreationForm/schema.ts @@ -0,0 +1,17 @@ +import * as Yup from 'yup'; + +const schema = Yup.object({ + startDate: Yup.string().required('Required'), + endDate: Yup.string().required('Required'), + name: Yup.string().required('Required'), + type: Yup.string().required('Required'), + location: Yup.string().required('Required'), + description: Yup.string().required('Required'), + hosts: Yup.array().required('Required'), + fbURL: Yup.string().url('Please either enter a valid URL or leave it blank'), + canvaURL: Yup.string().url( + 'Please either enter a valid URL or leave it blank' + ), +}); + +export default schema; diff --git a/src/pages/EventCreationPage/index.tsx b/src/pages/EventCreationPage/index.tsx new file mode 100644 index 00000000..dba47bdc --- /dev/null +++ b/src/pages/EventCreationPage/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useHistory } from 'react-router'; +import { Card } from '@material-ui/core'; + +import { EventCreationForm } from './components/EventCreationForm'; +import useStyles from './styles'; + +import * as ROUTES from '@Constants/routes'; +import { createEvent } from '@Services/EventService'; +import { EventRequest } from '@Services/api/models'; + +function EventCreationPage(): JSX.Element { + const history = useHistory(); + const classes = useStyles(); + + const handleSubmit = async ( + values: EventRequest, + setSubmitting: (_: boolean) => void + ) => { + const submission = { + ...values, + hosts: values.hosts.map(host => { + return { id: host.id }; + }), + fbURL: values.fbURL === '' ? undefined : values.fbURL, + canvaURL: values.canvaURL === '' ? undefined : values.canvaURL, + }; + + const createdEvent = await createEvent(submission); + const { id } = createdEvent; + + setSubmitting(false); + history.push(`/events/${id}`); + }; + + const handleCancel = () => { + history.push(ROUTES.CALENDAR); + }; + + return ( + + + + ); +} + +export default EventCreationPage; diff --git a/src/pages/EventCreationPage/styles.ts b/src/pages/EventCreationPage/styles.ts new file mode 100644 index 00000000..bfbce470 --- /dev/null +++ b/src/pages/EventCreationPage/styles.ts @@ -0,0 +1,9 @@ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + root: { + padding: '16px', + }, +}); + +export default useStyles; diff --git a/src/pages/EventDetailsPage/components/DeleteEditButtons/index.js b/src/pages/EventDetailsPage/components/DeleteEditButtons/index.js index 83c20080..0da06907 100644 --- a/src/pages/EventDetailsPage/components/DeleteEditButtons/index.js +++ b/src/pages/EventDetailsPage/components/DeleteEditButtons/index.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useHistory } from 'react-router'; import { Link } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; import DeleteIcon from '@material-ui/icons/Delete'; @@ -9,13 +10,14 @@ import styles from './styles'; import { Button, ButtonWithConfirmationModal } from '@SharedComponents'; import * as ROUTES from '@Constants/routes'; -import { deleteEventById } from '@Services/events'; +import { deleteEvent } from '@Services/EventService'; const DeleteEditButtons = props => { const { classes, eventId } = props; + const history = useHistory(); const handleDeleteEvent = eventToDeleteId => { - deleteEventById(eventToDeleteId) + deleteEvent(eventToDeleteId) .then(res => { return res; }) @@ -30,26 +32,30 @@ const DeleteEditButtons = props => { const confirmButtonProps = { name: 'Yes', - onClick: handleConfirmDelete, - to: ROUTES.CALENDAR, - component: Link, + onClick: () => { + handleConfirmDelete(); + history.push(ROUTES.CALENDAR); + }, + primary: true, positive: true, }; const cancelButtonProps = { name: 'No', + primary: true, positive: true, }; return (
} primary negative @@ -70,7 +76,7 @@ const DeleteEditButtons = props => { }; DeleteEditButtons.propTypes = { - eventId: PropTypes.string.isRequired, + eventId: PropTypes.number.isRequired, }; export default withStyles(styles)(DeleteEditButtons); diff --git a/src/pages/EventDetailsPage/components/DeleteEditButtons/styles.js b/src/pages/EventDetailsPage/components/DeleteEditButtons/styles.js index d77a63b9..021fc93e 100644 --- a/src/pages/EventDetailsPage/components/DeleteEditButtons/styles.js +++ b/src/pages/EventDetailsPage/components/DeleteEditButtons/styles.js @@ -3,9 +3,6 @@ const styles = () => ({ display: 'flex', flexDirection: 'row', alignItems: 'center', - marginTop: '10px', - marginBottom: '10px', - marginRight: '-22px', }, delete: { marginRight: '10px', diff --git a/src/pages/EventDetailsPage/components/EventDetails/Links.js b/src/pages/EventDetailsPage/components/EventDetails/Links.js deleted file mode 100644 index af507d1a..00000000 --- a/src/pages/EventDetailsPage/components/EventDetails/Links.js +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import { grey } from '@material-ui/core/colors'; -import { - List, - ListItem, - ListItemText, - Typography, - Link, -} from '@material-ui/core'; - -const styles = () => ({ - root: { - backgroundColor: grey[200], - }, - title: { - backgroundColor: grey[400], - }, - list_item_text: { - fontSize: '16px', - }, -}); - -function Links(props) { - const { classes, urls } = props; - const { fb, canva, rsvp, signin } = urls; - - // .map for list - return ( -
- - Links - - - - - - - - - - - - - - - - - - - - - - - - -
- ); -} - -Links.propTypes = { - urls: PropTypes.shape({ - fb: PropTypes.string.isRequired, - canva: PropTypes.string.isRequired, - rsvp: PropTypes.string.isRequired, - signin: PropTypes.string.isRequired, - }).isRequired, -}; - -export default withStyles(styles)(Links); diff --git a/src/pages/EventDetailsPage/components/EventDetails/index.js b/src/pages/EventDetailsPage/components/EventDetails/index.js deleted file mode 100644 index df5c80e0..00000000 --- a/src/pages/EventDetailsPage/components/EventDetails/index.js +++ /dev/null @@ -1,134 +0,0 @@ -import React from 'react'; -import { Typography, Container, Card, Button, Grid } from '@material-ui/core'; -import { withStyles } from '@material-ui/core/styles'; -import { Link } from 'react-router-dom'; -import { format } from 'date-fns'; -import PropTypes from 'prop-types'; - -import DeleteEditButtons from '../DeleteEditButtons'; - -import Links from './Links'; -import styles from './styles'; - -import { OfficerRenderPermission } from '@HOCs/RenderPermissions'; -import { Tags } from '@SharedComponents'; -import * as ROUTES from '@Constants/routes'; - -function EventDetailsComponent(props) { - const { classes, eventInfo, eventId } = props; - const { - endDate, - hosts, - location, - name, - startDate, - tags, - urls, - description, - } = eventInfo; - - const updatedUrls = urls; - updatedUrls.signin = `/events/${eventId}/signin`; - updatedUrls.rsvp = `/events/${eventId}/rsvp`; - - return ( -
- - - - - - {name} - - - - - - {OfficerRenderPermission(DeleteEditButtons)({ eventId })} - - - - - - - Hosts:{' '} - {hosts.map(host => ( - - {host} - - ))} - - - - - - Location: {location} - - - - Start Time:{' '} - {format(startDate, 'PPP p')} - - - End Time: {format(endDate, 'PPP p')} - - - - - - - {OfficerRenderPermission(Links)({ urls: updatedUrls })} - - - - - Description: {description} - - - - - - - -
- ); -} - -EventDetailsComponent.propTypes = { - eventInfo: PropTypes.shape({ - startDate: PropTypes.instanceOf(Date).isRequired, - endDate: PropTypes.instanceOf(Date).isRequired, - hosts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, - location: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - tags: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, - description: PropTypes.string.isRequired, - urls: PropTypes.shape({ - fb: PropTypes.string.isRequired, - canva: PropTypes.string.isRequired, - rsvp: PropTypes.string.isRequired, - signin: PropTypes.string.isRequired, - }), - }).isRequired, - eventId: PropTypes.string.isRequired, -}; - -export default withStyles(styles)(EventDetailsComponent); diff --git a/src/pages/EventDetailsPage/components/EventDetails/index.tsx b/src/pages/EventDetailsPage/components/EventDetails/index.tsx new file mode 100644 index 00000000..e57ab1f1 --- /dev/null +++ b/src/pages/EventDetailsPage/components/EventDetails/index.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import { Typography, Grid } from '@material-ui/core'; +import { useHistory } from 'react-router'; +import { format, parseISO } from 'date-fns'; + +import DeleteEditButtons from '../DeleteEditButtons'; +import Links from '../Links/Links'; +import SignInButton from '../SignInButton'; +import RSVPButton from '../RSVPButton'; + +import useStyles from './styles'; + +import { + OfficerRenderPermission, + InducteeRenderPermission, +} from '@HOCs/RenderPermissions'; +import * as ROUTES from '@Constants/routes'; +import { Tags, Card, Button } from '@SharedComponents'; +import { EventResponse as EventInfo } from '@Services/api/models'; + +interface EventDetailsComponentProps { + eventInfo: EventInfo; + eventId: number; +} + +function EventDetailsComponent(props: EventDetailsComponentProps) { + const { eventInfo, eventId } = props; + const classes = useStyles(); + const history = useHistory(); + + const { + endDate, + hosts, + location = '', + description, + name, + startDate, + type = 'Event', + fbURL = null, + canvaURL = null, + signInURL, + rsvpURL, + } = eventInfo; + + const urls = { + fb: { + url: fbURL, + label: 'Facebook', + }, + canva: { url: canvaURL, label: 'Canva' }, + }; + + return ( + + + + + + + + + {name} + + + + + + {OfficerRenderPermission(DeleteEditButtons)({ + eventId, + })} + + + + + + + + + Hosts:{' '} + {hosts.map(host => ( + + {`${host.firstName} ${host.lastName}`} + + ))} + + + + + + + + Location: {location} + + + + + + Start Time:{' '} + + {format(parseISO(startDate), 'PPP h:mm aaaa')} + + + + + + + End Time:{' '} + + {format(parseISO(endDate), 'PPP h:mm aaaa')} + + + + + + + + + + + + + + {InducteeRenderPermission(Links)({ + urls, + signIn: { url: signInURL, label: 'Sign In Form' }, + rsvp: { url: rsvpURL, label: 'RSVP Form' }, + })} + + + + + + + + + + + + + + + Description: {description} + + + + + + + + + + + + + ); +} + +export default EventDetailsComponent; diff --git a/src/pages/EventDetailsPage/components/EventDetails/styles.js b/src/pages/EventDetailsPage/components/EventDetails/styles.js deleted file mode 100644 index da9e7aab..00000000 --- a/src/pages/EventDetailsPage/components/EventDetails/styles.js +++ /dev/null @@ -1,40 +0,0 @@ -const styles = () => ({ - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - }, - container: { - backgroundColor: 'white', - }, - titleTag: { - marginTop: '10px', - marginBottom: '10px', - }, - title: { - wordWrap: 'break-word', - }, - hostLocTime: { - marginTop: '10px', - marginBottom: '10px', - }, - hosts: { - wordWrap: 'break-word', - }, - hostName: { - marginBottom: '5px', - }, - locTime: { - marginLeft: '10px', - }, - descURL: { - marginTop: '10px', - marginBottom: '10px', - }, - calendarButton: { - marginTop: '25px', - marginBottom: '25px', - }, -}); - -export default styles; diff --git a/src/pages/EventDetailsPage/components/EventDetails/styles.tsx b/src/pages/EventDetailsPage/components/EventDetails/styles.tsx new file mode 100644 index 00000000..98efeb09 --- /dev/null +++ b/src/pages/EventDetailsPage/components/EventDetails/styles.tsx @@ -0,0 +1,18 @@ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + eventDetailsCard: { + width: '536px', + }, + title: { + wordWrap: 'break-word', + }, + hosts: { + wordWrap: 'break-word', + }, + hostName: { + marginBottom: '5px', + }, +}); + +export default useStyles; diff --git a/src/pages/EventDetailsPage/components/Links/Links.tsx b/src/pages/EventDetailsPage/components/Links/Links.tsx new file mode 100644 index 00000000..c29b4b42 --- /dev/null +++ b/src/pages/EventDetailsPage/components/Links/Links.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { + List, + ListItem, + ListItemText, + Typography, + Link, +} from '@material-ui/core'; + +import useStyles from './styles'; + +import { OfficerRenderPermission } from '@HOCs/RenderPermissions'; + +interface URLObject { + url: string; + label: string; +} + +interface LinksProps { + urls: { + [key: string]: URLObject; + }; + signIn: URLObject; + rsvp: URLObject; +} + +function Links(props: LinksProps) { + const { urls, signIn, rsvp } = props; + const classes = useStyles(); + + const SignInLink = () => ( + + + + + + ); + + const RSVPLink = () => ( + + + + + + ); + + return ( +
+ + Links + + + + {Object.values(urls).map(urlObj => { + return ( + + + + + + ); + })} + + {OfficerRenderPermission(SignInLink)({})} + {OfficerRenderPermission(RSVPLink)({})} + +
+ ); +} + +export default Links; diff --git a/src/pages/EventDetailsPage/components/Links/styles.ts b/src/pages/EventDetailsPage/components/Links/styles.ts new file mode 100644 index 00000000..f1c24f7c --- /dev/null +++ b/src/pages/EventDetailsPage/components/Links/styles.ts @@ -0,0 +1,16 @@ +import { makeStyles } from '@material-ui/core'; +import { grey } from '@material-ui/core/colors'; + +const useStyles = makeStyles({ + root: { + backgroundColor: grey[200], + }, + title: { + backgroundColor: grey[400], + }, + list_item_text: { + fontSize: '16px', + }, +}); + +export default useStyles; diff --git a/src/pages/EventDetailsPage/components/RSVPButton.tsx b/src/pages/EventDetailsPage/components/RSVPButton.tsx new file mode 100644 index 00000000..4b7b0a21 --- /dev/null +++ b/src/pages/EventDetailsPage/components/RSVPButton.tsx @@ -0,0 +1,26 @@ +import { InducteeRenderPermission } from '@HOCs/RenderPermissions'; +import { Button } from '@SharedComponents'; +import { rsvpToEvent } from '@Services/EventService'; + +interface SignInButtonProps { + eventId: number; + children?: string; +} + +function RSVPButton({ eventId, children = 'RSVP' }: SignInButtonProps) { + const eventRequestPayloadFiller = { + email: 'filler@filler.filler', + firstName: '', + lastName: '', + major: '', + }; + + return InducteeRenderPermission(Button)({ + children, + primary: true, + positive: true, + onClick: () => rsvpToEvent(eventId, eventRequestPayloadFiller), + }); +} + +export default RSVPButton; diff --git a/src/pages/EventDetailsPage/components/SignInButton.ts b/src/pages/EventDetailsPage/components/SignInButton.ts new file mode 100644 index 00000000..b87360c1 --- /dev/null +++ b/src/pages/EventDetailsPage/components/SignInButton.ts @@ -0,0 +1,26 @@ +import { InducteeRenderPermission } from '@HOCs/RenderPermissions'; +import { Button } from '@SharedComponents'; +import { signInToEvent } from '@Services/EventService'; + +interface SignInButtonProps { + eventId: number; + children?: string; +} + +function SignInButton({ eventId, children = 'Sign In' }: SignInButtonProps) { + const eventRequestPayloadFiller = { + email: 'filler@filler.filler', + firstName: '', + lastName: '', + major: '', + }; + + return InducteeRenderPermission(Button)({ + children, + primary: true, + positive: true, + onClick: () => signInToEvent(eventId, eventRequestPayloadFiller), + }); +} + +export default SignInButton; diff --git a/src/pages/EventDetailsPage/index.js b/src/pages/EventDetailsPage/index.js deleted file mode 100644 index 77767fcc..00000000 --- a/src/pages/EventDetailsPage/index.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import EventDetailsComponent from './components/EventDetails'; - -import { Loading } from '@SharedComponents'; -import { getEventById } from '@Services/events'; - -class EventDetailsPage extends React.Component { - constructor(props) { - const { - match: { - params: { id }, - }, - } = props; - - super(props); - - this.state = { - eventId: id, - eventInfo: null, - }; - } - - componentDidMount() { - const { eventId } = this.state; - - getEventById(eventId) - .then(eventObj => { - this.setState({ eventInfo: eventObj }); - }) - .catch(err => { - console.log(err); - }); - } - - render() { - const { eventId, eventInfo } = this.state; - - const EventDetails = - eventInfo == null ? ( - - ) : ( - - ); - - return EventDetails; - } -} - -EventDetailsPage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape(PropTypes.string.isRequired).isRequired, - }).isRequired, -}; - -export default EventDetailsPage; diff --git a/src/pages/EventDetailsPage/index.tsx b/src/pages/EventDetailsPage/index.tsx new file mode 100644 index 00000000..03711854 --- /dev/null +++ b/src/pages/EventDetailsPage/index.tsx @@ -0,0 +1,35 @@ +import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router'; + +import EventDetailsComponent from './components/EventDetails'; + +import { Loading } from '@SharedComponents'; +import { getEventById } from '@Services/EventService'; +import { EventResponse } from '@Services/api/models'; + +interface EventID { + id: string; +} + +function EventDetailsPage(): JSX.Element { + const { id } = useParams(); + const eventId = parseInt(id, 10); + const [eventInfo, setEventInfo] = useState(null); + + useEffect(() => { + const getEvent = async () => { + const eventResponse = await getEventById(eventId); + setEventInfo(eventResponse); + }; + + getEvent(); + }, [eventId]); + + return eventInfo == null ? ( + + ) : ( + + ); +} + +export default EventDetailsPage; diff --git a/src/pages/EventEditPage/components/EventEditForm/edit_form.js b/src/pages/EventEditPage/components/EventEditForm/edit_form.js index 9c63b8b7..156231c4 100644 --- a/src/pages/EventEditPage/components/EventEditForm/edit_form.js +++ b/src/pages/EventEditPage/components/EventEditForm/edit_form.js @@ -9,22 +9,40 @@ import { MuiPickersUtilsProvider } from '@material-ui/pickers'; import DateFnsUtils from '@date-io/date-fns'; import * as Yup from 'yup'; -import FormikChipListInput from './form_chip_list_input'; -import FormikMultiChipSelect from './form_multi_chip_select'; import styles from './styles'; -import EVENT_TAGS from '@Constants/eventTags'; +import { + EventTypeDropdownField, + EventStatusDropdownField, + OfficerNameAutocomplete, +} from '@SharedComponents'; const schema = Yup.object({ name: Yup.string().required('Required'), hosts: Yup.array().required('Required'), location: Yup.string().required('Required'), - tags: Yup.array(), + type: Yup.string(), + status: Yup.string().required('Required'), description: Yup.string(), + fbURL: Yup.string(), + canvaURL: Yup.string(), }); const EventEditForm = props => { const { handleSubmit, handleCancel, classes, initialValues } = props; + const { fbURL, canvaURL } = initialValues; + const urlObjects = [ + { + name: 'fbURL', + label: 'Facebook URL', + url: fbURL, + }, + { + name: 'canvaURL', + label: 'Canva URL', + url: canvaURL, + }, + ]; return ( @@ -72,13 +90,29 @@ const EventEditForm = props => { label='Event Name' /> - + + + + + + + + + + + { fullWidth label='Location' /> - - - {Object.keys(initialValues.urls).map(url => { + {urlObjects.map(urlObject => { + const { name, label, url } = urlObject; + return ( ); })} @@ -164,20 +192,27 @@ EventEditForm.propTypes = { handleSubmit: PropTypes.func.isRequired, handleCancel: PropTypes.func.isRequired, initialValues: PropTypes.shape({ - startDate: PropTypes.instanceOf(Date).isRequired, - endDate: PropTypes.instanceOf(Date).isRequired, - hosts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, - location: PropTypes.string.isRequired, + startDate: PropTypes.string.isRequired, + endDate: PropTypes.string.isRequired, + hosts: PropTypes.array.isRequired, + location: PropTypes.string, name: PropTypes.string.isRequired, - tags: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, + type: PropTypes.string, + status: PropTypes.string.isRequired, description: PropTypes.string.isRequired, - urls: PropTypes.shape({ - fb: PropTypes.string.isRequired, - canva: PropTypes.string.isRequired, - rsvp: PropTypes.string.isRequired, - signin: PropTypes.string.isRequired, - }), - }).isRequired, + fbURL: PropTypes.string, + canvaURL: PropTypes.string, + rsvpURL: PropTypes.string.isRequired, + signInURL: PropTypes.string.isRequired, + }), +}; + +EventEditForm.defaultProps = { + initialValues: { + fbURL: '', + canvaURL: '', + type: '', + }, }; export default withStyles(styles)(EventEditForm); diff --git a/src/pages/EventEditPage/event_edit.js b/src/pages/EventEditPage/event_edit.js deleted file mode 100644 index e4823a7f..00000000 --- a/src/pages/EventEditPage/event_edit.js +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import EventEditForm from './components/EventEditForm'; - -import { getEventById, setEventDetails } from '@Services/events'; - -class EventEditPage extends React.Component { - constructor(props) { - super(props); - const { - match: { - params: { eventId }, - }, - } = props; - this.state = { eventId, initialValues: {}, formLoading: true }; - } - - componentDidMount() { - const { eventId } = this.state; - - getEventById(eventId).then(event => { - const initialValues = { - ...event, - startDate: event.startDate, - endDate: event.endDate, - }; - this.setState({ initialValues, formLoading: false }); - }); - } - - render() { - const { history } = this.props; - - const { eventId, initialValues, formLoading } = this.state; - - const handleCancel = () => { - history.push(`/events/${eventId}`); - }; - - const handleSubmit = (values, setSubmitting) => { - const parsedStartDate = new Date(values.startDate); - const parsedEndDate = new Date(values.endDate); - - const submission = { - ...values, - startDate: parsedStartDate, - endDate: parsedEndDate, - }; - setEventDetails(eventId, submission).then(() => { - setSubmitting(false); - history.push(`/events/${eventId}`); - }); - }; - - return ( -
- {formLoading ? ( -
- ) : ( - - )} -
- ); - } -} - -EventEditPage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - eventId: PropTypes.string.isRequired, - }), - }).isRequired, -}; - -export default EventEditPage; diff --git a/src/pages/EventEditPage/event_edit.tsx b/src/pages/EventEditPage/event_edit.tsx new file mode 100644 index 00000000..68f45da7 --- /dev/null +++ b/src/pages/EventEditPage/event_edit.tsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from 'react'; +import { useHistory, useParams } from 'react-router'; + +import EventEditForm from './components/EventEditForm'; + +import { Card } from '@SharedComponents'; +import { getEventById, updateEvent } from '@Services/EventService'; +import { EventResponse, EventRequest } from '@Services/api/models'; + +interface ParamTypes { + eventId: string; +} + +function EventEditPage(): JSX.Element { + const { eventId } = useParams(); + const id = parseInt(eventId, 10); + const history = useHistory(); + const [event, setEvent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const getEvent = async () => { + const eventResponse = await getEventById(id); + setEvent(eventResponse); + setLoading(false); + }; + getEvent(); + }, [id]); + + const handleCancel = () => { + history.push(`/events/${id}`); + }; + + const handleSubmit = async ( + values: EventRequest, + setSubmitting: (_: boolean) => void + ) => { + const eventRequest: EventRequest = { + ...values, + hosts: values.hosts.map(host => { + return { + id: host.id, + }; + }), + }; + + await updateEvent(id, eventRequest); + setSubmitting(false); + history.push(`/events/${id}`); + }; + + if (loading) { + return <>; + } + return ( + + + + ); +} + +export default EventEditPage; diff --git a/src/pages/EventEditPage/index.js b/src/pages/EventEditPage/index.ts similarity index 96% rename from src/pages/EventEditPage/index.js rename to src/pages/EventEditPage/index.ts index 2abbf6ea..b5a095af 100644 --- a/src/pages/EventEditPage/index.js +++ b/src/pages/EventEditPage/index.ts @@ -1,3 +1,3 @@ -import EventEditPage from './event_edit'; - -export default EventEditPage; +import EventEditPage from './event_edit'; + +export default EventEditPage; diff --git a/src/pages/EventRsvpPage/components/EventRsvpForm/index.js b/src/pages/EventRsvpPage/components/EventRsvpForm/index.js index 8bbbb0d3..f8a32d47 100644 --- a/src/pages/EventRsvpPage/components/EventRsvpForm/index.js +++ b/src/pages/EventRsvpPage/components/EventRsvpForm/index.js @@ -26,8 +26,9 @@ const EventRsvpForm = props => { { - handleSubmit(values, setSubmitting); + onSubmit={async (values, { setSubmitting, resetForm }) => { + await handleSubmit(values); + setSubmitting(false); resetForm({ values: '' }); }} > diff --git a/src/pages/EventRsvpPage/index.js b/src/pages/EventRsvpPage/index.js deleted file mode 100644 index 5590fdab..00000000 --- a/src/pages/EventRsvpPage/index.js +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Avatar, Card, Typography, Grid } from '@material-ui/core'; -import { withStyles } from '@material-ui/core/styles'; - -import EventRsvpForm from './components/EventRsvpForm'; -import styles from './styles'; - -import HKN_TRIDENT_LOGO from '@Images/hkn-trident.png'; -import { Loading } from '@SharedComponents'; -import { getEventById } from '@Services/events'; - -class EventRsvpPage extends React.Component { - constructor(props) { - const { - match: { - params: { id }, - }, - } = props; - - super(props); - - this.state = { - eventId: id, - eventInfo: null, - }; - } - - componentDidMount() { - const { eventId } = this.state; - - getEventById(eventId) - .then(eventObj => { - this.setState({ eventInfo: eventObj }); - }) - .catch(err => { - console.log(err); - }); - } - - handleSubmit = (values, setSubmitting) => { - console.log(values); - - setSubmitting(false); - }; - - render() { - const { classes } = this.props; - const { eventInfo } = this.state; - - const EventRsvp = - eventInfo == null ? ( - - ) : ( -
- - - - - - - - - - - {eventInfo.name} - - - - - Event RSVP - - - - - - - - - -
- ); - - return EventRsvp; - } -} - -EventRsvpPage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape(PropTypes.string.isRequired).isRequired, - }).isRequired, -}; - -export default withStyles(styles)(EventRsvpPage); diff --git a/src/pages/EventRsvpPage/index.tsx b/src/pages/EventRsvpPage/index.tsx new file mode 100644 index 00000000..6df591c3 --- /dev/null +++ b/src/pages/EventRsvpPage/index.tsx @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router'; +import { Avatar, Typography, Grid } from '@material-ui/core'; + +import EventRsvpForm from './components/EventRsvpForm'; +import useStyles from './styles'; + +import HKN_TRIDENT_LOGO from '@Images/hkn-trident.png'; +import { Loading, Card, PublicPageLayout } from '@SharedComponents'; +import { getEventById, rsvpToEvent } from '@Services/EventService'; +import { EventResponse, AppUserEventRequest } from '@Services/api/models'; + +interface ParamTypes { + id: string; +} + +function EventRsvpPage(): JSX.Element { + const { id } = useParams(); + const eventID = parseInt(id, 10); + const [event, setEvent] = useState(null); + const classes = useStyles(); + + useEffect(() => { + const getEvent = async () => { + const eventResponse = await getEventById(eventID); + setEvent(eventResponse); + }; + getEvent(); + }, [eventID]); + + return event == null ? ( + + ) : ( + + + + + + + + + + + + {event.name} + + + + + Event RSVP + + + + + + + rsvpToEvent(eventID, appUserEventRequest) + } + /> + + + + + ); +} + +export default EventRsvpPage; diff --git a/src/pages/EventRsvpPage/styles.js b/src/pages/EventRsvpPage/styles.ts similarity index 78% rename from src/pages/EventRsvpPage/styles.js rename to src/pages/EventRsvpPage/styles.ts index 62e476e9..b63051e3 100644 --- a/src/pages/EventRsvpPage/styles.js +++ b/src/pages/EventRsvpPage/styles.ts @@ -1,4 +1,6 @@ -const styles = () => ({ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ root: { display: 'flex', flexDirection: 'column', @@ -22,4 +24,4 @@ const styles = () => ({ }, }); -export default styles; +export default useStyles; diff --git a/src/pages/EventSignInPage/components/EventSignInForm/index.js b/src/pages/EventSignInPage/components/EventSignInForm/index.js index 69b09681..be0d82fd 100644 --- a/src/pages/EventSignInPage/components/EventSignInForm/index.js +++ b/src/pages/EventSignInPage/components/EventSignInForm/index.js @@ -26,8 +26,9 @@ const EventSignInForm = props => { { - handleSubmit(values, setSubmitting); + onSubmit={async (values, { setSubmitting, resetForm }) => { + await handleSubmit(values); + setSubmitting(false); resetForm({ values: '' }); }} > diff --git a/src/pages/EventSignInPage/index.js b/src/pages/EventSignInPage/index.js deleted file mode 100644 index 44cbaec3..00000000 --- a/src/pages/EventSignInPage/index.js +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Avatar, Card, Typography, Grid } from '@material-ui/core'; -import { withStyles } from '@material-ui/core/styles'; - -import EventSignInForm from './components/EventSignInForm'; -import styles from './styles'; - -import HKN_TRIDENT_LOGO from '@Images/hkn-trident.png'; -import { Loading } from '@SharedComponents'; -import { getEventById } from '@Services/events'; - -class EventSignInPage extends React.Component { - constructor(props) { - const { - match: { - params: { id }, - }, - } = props; - - super(props); - - this.state = { - eventId: id, - eventInfo: null, - }; - } - - componentDidMount() { - const { eventId } = this.state; - - getEventById(eventId) - .then(eventObj => { - this.setState({ eventInfo: eventObj }); - }) - .catch(err => { - console.log(err); - }); - } - - handleSubmit = (values, setSubmitting) => { - console.log(values); - - setSubmitting(false); - }; - - render() { - const { classes } = this.props; - const { eventInfo } = this.state; - - const EventSignIn = - eventInfo == null ? ( - - ) : ( -
- - - - - - - - - - - {eventInfo.name} - - - - - Event Sign In - - - - - - - - - -
- ); - - return EventSignIn; - } -} - -EventSignInPage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape(PropTypes.string.isRequired).isRequired, - }).isRequired, -}; - -export default withStyles(styles)(EventSignInPage); diff --git a/src/pages/EventSignInPage/index.tsx b/src/pages/EventSignInPage/index.tsx new file mode 100644 index 00000000..44933d36 --- /dev/null +++ b/src/pages/EventSignInPage/index.tsx @@ -0,0 +1,72 @@ +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; +import { Avatar, Typography, Grid } from '@material-ui/core'; + +import EventSignInForm from './components/EventSignInForm'; +import useStyles from './styles'; + +import HKN_TRIDENT_LOGO from '@Images/hkn-trident.png'; +import { Loading, Card, PublicPageLayout } from '@SharedComponents'; +import { getEventById, signInToEvent } from '@Services/EventService'; +import { EventResponse, AppUserEventRequest } from '@Services/api/models'; + +interface ParamTypes { + id: string; +} + +function EventSignInPage(): JSX.Element { + const { id } = useParams(); + const eventID = parseInt(id, 10); + const [event, setEvent] = useState(null); + const classes = useStyles(); + + useEffect(() => { + const getEvent = async () => { + const eventResponse: EventResponse = await getEventById(eventID); + setEvent(eventResponse); + }; + getEvent(); + }, [eventID]); + + return event == null ? ( + + ) : ( + + + + + + + + + + + + {event.name} + + + + + Event Sign In + + + + + + + signInToEvent(eventID, values) + } + /> + + + + + ); +} + +export default EventSignInPage; diff --git a/src/pages/EventSignInPage/styles.js b/src/pages/EventSignInPage/styles.ts similarity index 79% rename from src/pages/EventSignInPage/styles.js rename to src/pages/EventSignInPage/styles.ts index ed541d1e..e49871bf 100644 --- a/src/pages/EventSignInPage/styles.js +++ b/src/pages/EventSignInPage/styles.ts @@ -1,4 +1,6 @@ -const styles = () => ({ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ root: { display: 'flex', flexDirection: 'column', @@ -22,4 +24,4 @@ const styles = () => ({ }, }); -export default styles; +export default useStyles; diff --git a/src/pages/InducteePointsPage/PointDetail.js b/src/pages/InducteePointsPage/PointDetail.js index a83ccb3b..8b297166 100644 --- a/src/pages/InducteePointsPage/PointDetail.js +++ b/src/pages/InducteePointsPage/PointDetail.js @@ -3,16 +3,11 @@ import React from 'react'; import { compose } from 'recompose'; import PropTypes from 'prop-types'; -import { - Grid, - Card, - CardHeader, - CardContent, - Typography, -} from '@material-ui/core'; +import { Grid, Typography } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import { withStyles } from '@material-ui/core/styles'; +import { Card } from '@SharedComponents'; import { getUserEvent } from '@Services/events'; const styles = theme => ({ @@ -56,23 +51,17 @@ class PointDetail extends React.Component { alignItems='flex-start' > - - - - {eventNames.map(eventName => ( - {eventName} - ))} - + + {eventNames.map(eventName => ( + {eventName} + ))} - - - - {officerSigns.map(officerName => ( - {officerName} - ))} - + + {officerSigns.map(officerName => ( + {officerName} + ))} diff --git a/src/pages/PointsPage/point_display.js b/src/pages/PointsPage/point_display.js index e4baa1c4..84c5dd2b 100644 --- a/src/pages/PointsPage/point_display.js +++ b/src/pages/PointsPage/point_display.js @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; -import { compose } from 'recompose'; -import { Card, CardContent, Typography, Grid } from '@material-ui/core'; +import { Typography, Grid } from '@material-ui/core'; import { format } from 'date-fns'; +import { Card } from '@SharedComponents'; + const styles = () => ({ card: { minWidth: 200, @@ -59,22 +60,17 @@ class PointDisplay extends React.Component { key={event.event_name} > - - - {event.event_name} - - - {format(event.date, 'PP')} - - - {`Officer: ${event.officer}`} -
- {`Points: ${event.value}`} -
-
+ + {event.event_name} + + + {format(event.date, 'PP')} + + + {`Officer: ${event.officer}`} +
+ {`Points: ${event.value}`} +
); @@ -93,4 +89,4 @@ PointDisplay.propTypes = { points: PropTypes.arrayOf(PropTypes.object).isRequired, }; -export default compose(withStyles(styles))(PointDisplay); +export default withStyles(styles)(PointDisplay); diff --git a/src/pages/ProfileEditPage/index.js b/src/pages/ProfileEditPage/index.js deleted file mode 100644 index 7250382d..00000000 --- a/src/pages/ProfileEditPage/index.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import { Card, CardContent, Grid } from '@material-ui/core'; -import { Formik, Form } from 'formik'; - -import schema from './schema'; -import styles from './styles'; - -import { FormLayout } from '@SharedComponents'; -import { - getAccountSection, - getPersonalInfoSection, -} from '@SharedComponents/formSections'; -import { PROFILE_WITH_ID } from '@Constants/routes'; - -class ProfileEditPage extends React.Component { - constructor(props) { - super(props); - this.state = { - profile: null, - }; - } - - componentDidMount() { - // const { - // match: { - // params: { id }, - // }, - // } = this.props; - - this.setState({ - profile: { - firstName: 'Godwin', - lastName: 'Pang', - email: 'gypang@ucsd.edu', - major: 'Computer Engineering - ECE', - gradYear: 2021, - }, - }); - } - - render() { - const { - classes, - history, - match: { - params: { id }, - }, - } = this.props; - const { profile } = this.state; - - if (profile === null) { - return <>; - } - - const handleSave = (newProfile, setSubmitting) => { - // call API to save new profile - setSubmitting(false); - history.push(PROFILE_WITH_ID(id)); - }; - - const handleCancel = () => { - // TODO maybe throw up a modal - history.push(PROFILE_WITH_ID(id)); - }; - - const sections = [getAccountSection(), getPersonalInfoSection()]; - return ( - { - handleSave(values, setSubmitting); - }} - > - {({ submitForm }) => ( -
- - - - - - - -
- )} -
- ); - } -} - -ProfileEditPage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - id: PropTypes.string.isRequired, - }), - }).isRequired, -}; - -export default withStyles(styles)(ProfileEditPage); diff --git a/src/pages/ProfileEditPage/index.tsx b/src/pages/ProfileEditPage/index.tsx new file mode 100644 index 00000000..cadb33cb --- /dev/null +++ b/src/pages/ProfileEditPage/index.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import { useParams, useHistory } from 'react-router'; +import { Grid } from '@material-ui/core'; +import { Formik, Form } from 'formik'; + +import schema from './schema'; +import useStyles from './styles'; + +import { Card, FormLayout } from '@SharedComponents'; +import { + getAccountSection, + getPersonalInfoSection, +} from '@SharedComponents/formSections'; +import { PROFILE_WITH_ID } from '@Constants/routes'; +import { + AppUserResponse, + AppUserResponseRoleEnum, + AppUserPostRequest, +} from '@Services/api/models'; + +function ProfileEditPage(): JSX.Element { + const [profile, setProfile] = useState(null); + const { id } = useParams(); + const history = useHistory(); + const classes = useStyles(); + + setProfile({ + id: 1, + firstName: 'Godwin', + lastName: 'Pang', + email: 'gypang@ucsd.edu', + major: 'Computer Engineering - ECE', + graduationYear: '2021', + role: AppUserResponseRoleEnum.Officer, + }); + + if (profile === null) { + return <>; + } + + const handleSave = ( + newProfile: AppUserPostRequest, + setSubmitting: (_: boolean) => void + ) => { + // call API to save new profile + setSubmitting(false); + history.push(PROFILE_WITH_ID(id)); + }; + + const handleCancel = () => { + // TODO maybe throw up a modal + history.push(PROFILE_WITH_ID(id)); + }; + + const sections = [getAccountSection(), getPersonalInfoSection()]; + return ( + { + // TODO fix this + handleSave((values as unknown) as AppUserPostRequest, setSubmitting); + }} + > + {({ submitForm }) => ( +
+ + + + + +
+ )} +
+ ); +} + +export default ProfileEditPage; diff --git a/src/pages/ProfileEditPage/schema.js b/src/pages/ProfileEditPage/schema.ts similarity index 100% rename from src/pages/ProfileEditPage/schema.js rename to src/pages/ProfileEditPage/schema.ts diff --git a/src/pages/ProfilePage/styles.js b/src/pages/ProfileEditPage/styles.ts similarity index 53% rename from src/pages/ProfilePage/styles.js rename to src/pages/ProfileEditPage/styles.ts index cc8ad185..4aa445de 100644 --- a/src/pages/ProfilePage/styles.js +++ b/src/pages/ProfileEditPage/styles.ts @@ -1,4 +1,6 @@ -const styles = () => ({ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ root: { width: '100%', }, @@ -10,4 +12,4 @@ const styles = () => ({ }, }); -export default styles; +export default useStyles; diff --git a/src/pages/ProfilePage/index.js b/src/pages/ProfilePage/index.js deleted file mode 100644 index 6c3a231c..00000000 --- a/src/pages/ProfilePage/index.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import { Card, CardContent, Grid } from '@material-ui/core'; - -import styles from './styles'; - -import { FormLayout } from '@SharedComponents'; -import { - getAccountSection, - getPersonalInfoSection, -} from '@SharedComponents/formSections'; -import { PROFILE_EDIT_WITH_ID } from '@Constants/routes'; - -class ProfilePage extends React.Component { - constructor(props) { - super(props); - this.state = { - profile: null, - }; - } - - componentDidMount() { - // const { - // match: { - // params: { id }, - // }, - // } = this.props; - // make db query here - this.setState({ - profile: { - firstName: 'Godwin', - lastName: 'Pang', - email: 'gypang@ucsd.edu', - major: 'Computer Engineering', - gradYear: 2021, - }, - }); - } - - render() { - const { - classes, - match: { - params: { id }, - }, - } = this.props; - const { profile } = this.state; - - if (profile === null) { - return <>; - } - - const { firstName, lastName, email, major, gradYear } = profile; - const sections = [ - getAccountSection({ readOnly: true, email }), - getPersonalInfoSection({ - readOnly: true, - firstName, - lastName, - major, - gradYear, - }), - ]; - - return ( - - - - - - - - ); - } -} - -export default withStyles(styles)(ProfilePage); - -ProfilePage.propTypes = { - match: PropTypes.shape({ - params: PropTypes.shape({ - id: PropTypes.string.isRequired, - }), - }).isRequired, -}; diff --git a/src/pages/ProfilePage/index.tsx b/src/pages/ProfilePage/index.tsx new file mode 100644 index 00000000..1a7e270d --- /dev/null +++ b/src/pages/ProfilePage/index.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import { Grid } from '@material-ui/core'; + +import useStyles from './styles'; + +import { Card, FormLayout } from '@SharedComponents'; +import { + getAccountSection, + getPersonalInfoSection, +} from '@SharedComponents/formSections'; +import { PROFILE_EDIT_WITH_ID } from '@Constants/routes'; +import { AppUserResponse, AppUserResponseRoleEnum } from '@Services/api/models'; + +function ProfilePage(): JSX.Element { + const [profile, setProfile] = useState(null); + const classes = useStyles(); + + setProfile({ + id: 1, + firstName: 'Godwin', + lastName: 'Pang', + email: 'gypang@ucsd.edu', + major: 'Computer Engineering', + graduationYear: '2021', + role: AppUserResponseRoleEnum.Officer, + }); + + if (profile === null) { + return <>; + } + + const sections = [ + getAccountSection({ readOnly: true, ...profile }), + getPersonalInfoSection({ + readOnly: true, + ...profile, + }), + ]; + + return ( + + + + + + ); +} + +export default ProfilePage; diff --git a/src/pages/ProfileEditPage/styles.js b/src/pages/ProfilePage/styles.ts similarity index 53% rename from src/pages/ProfileEditPage/styles.js rename to src/pages/ProfilePage/styles.ts index cc8ad185..4aa445de 100644 --- a/src/pages/ProfileEditPage/styles.js +++ b/src/pages/ProfilePage/styles.ts @@ -1,4 +1,6 @@ -const styles = () => ({ +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ root: { width: '100%', }, @@ -10,4 +12,4 @@ const styles = () => ({ }, }); -export default styles; +export default useStyles; diff --git a/src/pages/QueriedEventPage/index.tsx b/src/pages/QueriedEventPage/index.tsx new file mode 100644 index 00000000..48e52171 --- /dev/null +++ b/src/pages/QueriedEventPage/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useLocation } from 'react-router'; +import queryString from 'query-string'; + +import EventCreationPage from '../EventCreationPage'; + +import { OfficerRoutingPermission } from '@HOCs/RoutingPermissions'; + +/* + * This is for conditional rendering of component(s) at /events endpoint based on + * the endpoint's query parameters + */ +function QueriedEventPage(): JSX.Element { + const location = useLocation(); + const values = queryString.parse(location.search); + const { create } = values; + + let PageToReturn = <>; + + if (create === 'true') { + PageToReturn = OfficerRoutingPermission(EventCreationPage)({}); + } + + return PageToReturn; +} + +export default QueriedEventPage; diff --git a/src/pages/SignInPage/index.js b/src/pages/SignInPage/index.js index ceff7009..65e5c2d3 100644 --- a/src/pages/SignInPage/index.js +++ b/src/pages/SignInPage/index.js @@ -31,7 +31,7 @@ import { doSignOut, doSendVerificationEmail, doPasswordReset, - getCurrentUserClaims, + getCurrentUserIDAndToken, } from '@Services/auth'; const styles = theme => ({ @@ -118,29 +118,17 @@ class SignInPage extends React.Component { this.state = { ...INITIAL_STATE }; } - componentDidMount() { - const { history } = this.props; - this.listener = firebase.auth().onAuthStateChanged(authUser => { - if (authUser && authUser.isEmailVerified) { - history.push(ROUTES.HOME); - } - }); - } - - componentWillUnmount() { - this.listener(); - } - handleSignIn = event => { const { email, password, checked } = this.state; const { history, setClaims } = this.props; doSignInWithEmailAndPassword(email, password, checked) .then(() => { - return getCurrentUserClaims(); + return getCurrentUserIDAndToken(); }) - .then(claims => { - return setClaims(claims); + .then(authObj => { + const { userID, token } = authObj; + return setClaims(userID, token); }) .then(() => { if (firebase.auth().currentUser.emailVerified) { diff --git a/src/pages/SignUpPage/components/SignUpForm/index.js b/src/pages/SignUpPage/components/SignUpForm/index.js index 5af3ebb9..5a05c684 100644 --- a/src/pages/SignUpPage/components/SignUpForm/index.js +++ b/src/pages/SignUpPage/components/SignUpForm/index.js @@ -35,12 +35,13 @@ const SignUpForm = props => { > {({ submitForm, isSubmitting }) => (
- + - + @@ -49,6 +50,7 @@ const SignUpForm = props => { @@ -57,53 +59,53 @@ const SignUpForm = props => { - - - - + + - - - + + + - - - - + + - + - + - +