├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── amplify ├── .config │ └── project-config.json └── backend │ ├── analytics │ └── surveypwa │ │ ├── parameters.json │ │ └── pinpoint-cloudformation-template.json │ ├── api │ └── surveypwa │ │ ├── parameters.json │ │ ├── schema.graphql │ │ └── stacks │ │ └── CustomResources.json │ ├── auth │ └── surveypwa1a7615c6 │ │ ├── parameters.json │ │ └── surveypwa1a7615c6-cloudformation-template.yml │ ├── backend-config.json │ └── function │ └── surveypwa1a7615c6PostConfirmation │ ├── function-parameters.json │ ├── parameters.json │ ├── src │ ├── add-to-group.js │ ├── event.json │ ├── index.js │ ├── package-lock.json │ └── package.json │ └── surveypwa1a7615c6PostConfirmation-cloudformation-template.json ├── package.json ├── public ├── Deck_Clock.png ├── favicon.ico ├── index.html ├── manifest.json └── simpsons.jpg └── src ├── assets ├── header.png └── surveytoolarchitecture.png ├── components ├── addentry │ └── index.js ├── admin │ ├── groups.js │ ├── index.js │ ├── question.js │ ├── questionnaire.js │ ├── survey.js │ └── users.js ├── app │ ├── App.css │ └── index.js ├── home │ └── index.js ├── multistep │ └── index.js ├── profile │ └── index.js ├── questionBool │ └── index.js ├── questionList │ └── index.js ├── questionText │ └── index.js ├── questionnaire │ └── index.js ├── settings │ └── index.js └── survey │ └── index.js ├── graphql ├── bulk.js ├── mutations.js ├── queries.js └── schema.json ├── index.css ├── index.js ├── logo.svg └── serviceWorker.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # amplify 26 | amplify/\#current-cloud-backend/ 27 | amplify/.config/local-* 28 | amplify/backend/amplify-meta.json 29 | amplify/backend/awscloudformation 30 | amplify/team-provider-info.json 31 | build/ 32 | dist/ 33 | node_modules/ 34 | aws-exports.js 35 | awsconfiguration.json -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-appsync-survey-tool/issues), or [recently closed](https://github.com/aws-samples/aws-appsync-survey-tool/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-appsync-survey-tool/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-appsync-survey-tool/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Appsync Survey Tool 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Appsync Survey Tool 2 | 3 | Sample Survey Tool Progressive Web Application written with React, GraphQL, AWS AppSync & AWS Amplify 4 | 5 | [![amplifybutton](https://oneclick.amplifyapp.com/button.svg)](https://console.aws.amazon.com/amplify/home#/deploy?repo=https://github.com/aws-samples/aws-appsync-survey-tool) 6 | 7 | ![](src/assets/header.png) 8 | 9 | 10 | ## Features 11 | 12 | - Full Progressive Web Application (PWA) 13 | - Install (desktop) or Add to Homescreen (mobile) 14 | - Offline ready 15 | - Adminstration Portal 16 | - User management 17 | 18 | 19 | ## Technologies 20 | 21 | - AWS AppSync 22 | - AWS Amplify 23 | - GraphQL 24 | - React Router 25 | - React Apollo 26 | - Material UI 27 | 28 | --- 29 | 30 | ## Quicklinks 31 | 32 | - [Introduction](#introduction) 33 | - [Getting Started](#getting-started) 34 | - [Prerequisites](#prerequisites) 35 | - [Automated Setup](#automated-setup) 36 | - [Manual Setup](#manual-setup) 37 | - [Clean Up](#clean-up) 38 | 39 | --- 40 | 41 | ## Introduction 42 | 43 | This is a demonstration solution that uses AWS AppSync to implement a survey app as a [Progressive Web Application](https://developers.google.com/web/progressive-web-apps/) (PWA). In this app, users can complete assigned surveys, including pre and post questionnaires. The solution also includes an administration portal, which allows admins to create and manage surveys and questionnaires. The solution demonstrates GraphQL capabilities (e.g. Mutations, Queries and Subscriptions) with AWS AppSync, offline support with the AWS AppSync SDK and React Apollo, and integrates with other AWS Services such as: 44 | - Amazon Cognito for user management, as well as Auth N/Z 45 | - Amazon DynamoDB with NoSQL Data Sources 46 | - Amazon S3 for asset storage 47 | - Amazon Pinpoint for web client analytics data collection 48 | 49 | ![](src/assets/surveytoolarchitecture.png) 50 | 51 | ## Getting Started 52 | 53 | ### Prerequisites 54 | 55 | - [AWS Account](https://aws.amazon.com/) with appropriate permissions to create the related resources 56 | - [NodeJS](https://nodejs.org/en/download/) with [NPM](https://docs.npmjs.com/getting-started/installing-node) 57 | - [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) with output configured as JSON `(pip install awscli --upgrade --user)` 58 | - [AWS Amplify CLI](https://github.com/aws-amplify/amplify-cli) configured for a region where [AWS AppSync](https://docs.aws.amazon.com/general/latest/gr/rande.html) and all other services in use are available `(npm install -g @aws-amplify/cli)` 59 | - [Create React App](https://github.com/facebook/create-react-app) `(npm install -g create-react-app)` 60 | 61 | 62 | ### Automated Setup 63 | 64 | ***_This process will use the configuration in the amplify folder of this repo._*** 65 | 66 | 1. First, clone this repository and navigate to the created folder: 67 | 68 | ```bash 69 | git clone https://github.com/aws-samples/aws-appsync-survey-tool.git 70 | 71 | cd aws-appsync-survey-tool 72 | ``` 73 | 74 | 2. Install the required modules: 75 | 76 | ```bash 77 | npm install 78 | ``` 79 | 80 | 3. Initilize the directory as an Amplify **Javascript** app using the **React** framework: 81 | 82 | ```bash 83 | amplify init 84 | ``` 85 | 86 | 4. Now it's time to provision your cloud resources based on the local setup and configured features. When asked to generate code, answer **"NO"** as it would overwrite the current custom files in the `src/graphql` folder. 87 | 88 | ```bash 89 | amplify push 90 | ``` 91 | 92 | Wait for the provisioning to complete. Once done, a `src/aws-exports.js` file with the resources information is created. 93 | 94 | 5. Run the project locally: 95 | 96 | ```bash 97 | npm start 98 | ``` 99 | --- 100 | 101 | ### Manual Setup 102 | 103 | ***_This process lets you configure custom settings for your backend components._*** 104 | 105 | 1. First, clone this repository and navigate to the created folder: 106 | 107 | ```bash 108 | git clone https://github.com/aws-samples/aws-appsync-survey-tool.git 109 | 110 | cd aws-appsync-survey-tool 111 | ``` 112 | 113 | 2. Install the required modules: 114 | 115 | ```bash 116 | npm install 117 | ``` 118 | 119 | 3. Delete the amplify folder 120 | 121 | ```bash 122 | rm -f amplify 123 | ``` 124 | 125 | 4. Init the directory as an amplify **Javascript** app using the **React** framework: 126 | 127 | ```bash 128 | amplify init 129 | ``` 130 | 131 | 5. Add an **Amazon Cognito User Pool** auth resource. Use the default configuration. 132 | 133 | ```bash 134 | amplify add auth 135 | ``` 136 | 137 | 6. Add an **AppSync GraphQL** API with **Amazon Cognito User Pool** for the API Authentication. Follow the default options. When prompted with "_Do you have an annotated GraphQL schema?_", select **"YES"** and provide the schema file path `backend/schema.graphql` 138 | 139 | ```bash 140 | amplify add api 141 | ``` 142 | 143 | 7. Now it's time to provision your cloud resources based on the local setup and configured features. When asked to generate code, answer **"NO"** as it would overwrite the current custom files in the `src/graphql` folder. 144 | 145 | ```bash 146 | amplify push 147 | ``` 148 | 149 | Wait for the provisioning to complete. Once done, a `src/aws-exports.js` file with the resources information is created. 150 | 151 | 8. Run the project locally: 152 | 153 | ```bash 154 | npm start 155 | ``` 156 | 157 | --- 158 | 159 | ### Clean Up 160 | 161 | To clean up the project use: 162 | 163 | ```bash 164 | amplify delete 165 | ``` 166 | 167 | to delete the resources created by the Amplify CLI. 168 | 169 | --- 170 | 171 | ## Change Log 172 | 173 | **1.0.0:** 174 | * Initial release. 175 | 176 | --- 177 | 178 | ## License 179 | 180 | This library is licensed under the Apache 2.0 License. -------------------------------------------------------------------------------- /amplify/.config/project-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "SurveyPWA", 3 | "version": "2.0", 4 | "frontend": "javascript", 5 | "javascript": { 6 | "framework": "react", 7 | "config": { 8 | "SourceDir": "src", 9 | "DistributionDir": "build", 10 | "BuildCommand": "npm run-script build", 11 | "StartCommand": "npm run-script start" 12 | } 13 | }, 14 | "providers": [ 15 | "awscloudformation" 16 | ] 17 | } -------------------------------------------------------------------------------- /amplify/backend/analytics/surveypwa/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "surveypwa", 3 | "roleName": "pinpointLambdaRole4b91d4de", 4 | "cloudWatchPolicyName": "cloudWatchPolicy4b91d4de", 5 | "pinpointPolicyName": "pinpointPolicy4b91d4de", 6 | "authPolicyName": "pinpoint_amplify_4b91d4de", 7 | "unauthPolicyName": "pinpoint_amplify_4b91d4de", 8 | "authRoleName": { 9 | "Ref": "AuthRoleName" 10 | }, 11 | "unauthRoleName": { 12 | "Ref": "UnauthRoleName" 13 | }, 14 | "authRoleArn": { 15 | "Fn::GetAtt": [ 16 | "AuthRole", 17 | "Arn" 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /amplify/backend/api/surveypwa/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSyncApiName": "surveypwa", 3 | "DynamoDBBillingMode": "PAY_PER_REQUEST", 4 | "AuthCognitoUserPoolId": { 5 | "Fn::GetAtt": [ 6 | "authsurveypwa1a7615c6", 7 | "Outputs.UserPoolId" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /amplify/backend/api/surveypwa/schema.graphql: -------------------------------------------------------------------------------- 1 | type Survey 2 | @model (subscriptions: { level: off }) 3 | @auth (rules: [ 4 | {allow: groups, groups: ["SurveyAdmins"]}, 5 | {allow: groups, groupsField: "groups", operations: [read]} 6 | ]) 7 | { 8 | id: ID! 9 | name: String! 10 | description: String! 11 | image: AWSURL 12 | preQuestionnaire: Questionnaire @connection 13 | mainQuestionnaire: Questionnaire @connection 14 | postQuestionnaire: Questionnaire @connection 15 | archived: Boolean 16 | groups: [String]! 17 | } 18 | 19 | type Questionnaire 20 | @model (subscriptions: { level: off }) 21 | @auth (rules: [{allow: groups, groups: ["Users"]}]) 22 | { 23 | id: ID! 24 | name: String! 25 | description: String! 26 | type: QuestionnaireType 27 | question: [Question] @connection(name: "QuestionnaireQuestions") 28 | } 29 | 30 | enum QuestionnaireType { 31 | PRE 32 | POST 33 | MAIN 34 | } 35 | 36 | type Question 37 | @model (subscriptions: { level: off }) 38 | @auth (rules: [{allow: groups, groups: ["Users"]}]) 39 | { 40 | id: ID! 41 | qu: String! 42 | type: QuestionType! 43 | listOptions: [String] 44 | questionnaire: Questionnaire @connection(name: "QuestionnaireQuestions") 45 | order: Int 46 | } 47 | 48 | enum QuestionType { 49 | LIST 50 | BOOL 51 | TEXT 52 | DATETIME 53 | } 54 | 55 | type Responses 56 | @model (subscriptions: { level: off }) 57 | @auth(rules: [{allow: owner}]) 58 | { 59 | id: ID! 60 | qu: Question! @connection 61 | res: String! 62 | group: SurveyEntries @connection(name: "SurveyEntryResponses") 63 | } 64 | 65 | type SurveyEntries 66 | @model (subscriptions: { level: off }) 67 | @auth(rules: [{allow: owner}]) 68 | { 69 | id: ID! 70 | responses: [Responses] @connection(name: "SurveyEntryResponses") 71 | } 72 | 73 | type Query 74 | { 75 | listUsers(UserPoolId: String): String 76 | listGroups(UserPoolId: String): String 77 | listGroupMembers(UserPoolId: String, GroupName: String): String 78 | } 79 | 80 | type Mutation 81 | { 82 | deleteUser(UserPoolId: String, Username: String): String 83 | addUserToGroup(UserPoolId: String, Username: String, GroupName: String): String 84 | addGroup(UserPoolId: String, GroupName: String): String 85 | deleteGroup(UserPoolId: String, GroupName: String): String 86 | } -------------------------------------------------------------------------------- /amplify/backend/api/surveypwa/stacks/CustomResources.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "An auto-generated nested stack. Updated", 4 | "Metadata": {}, 5 | "Parameters": { 6 | "AppSyncApiId": { 7 | "Type": "String", 8 | "Description": "The id of the AppSync API associated with this project." 9 | }, 10 | "AppSyncApiName": { 11 | "Type": "String", 12 | "Description": "The name of the AppSync API", 13 | "Default": "AppSyncSimpleTransform" 14 | }, 15 | "env": { 16 | "Type": "String", 17 | "Description": "The environment name. e.g. Dev, Test, or Production", 18 | "Default": "NONE" 19 | }, 20 | "S3DeploymentBucket": { 21 | "Type": "String", 22 | "Description": "The S3 bucket containing all deployment assets for the project." 23 | }, 24 | "S3DeploymentRootKey": { 25 | "Type": "String", 26 | "Description": "An S3 key relative to the S3DeploymentBucket that points to the root\nof the deployment directory." 27 | } 28 | }, 29 | "Resources": { 30 | "awsAppSyncServiceRole": { 31 | "Type": "AWS::IAM::Role", 32 | "Properties": { 33 | "AssumeRolePolicyDocument": { 34 | "Version": "2012-10-17", 35 | "Statement": [ 36 | { 37 | "Effect": "Allow", 38 | "Principal": { 39 | "Service": [ 40 | "appsync.amazonaws.com" 41 | ] 42 | }, 43 | "Action": [ 44 | "sts:AssumeRole" 45 | ] 46 | } 47 | ] 48 | }, 49 | "Path": "/" 50 | } 51 | }, 52 | "CognitoAccessPolicy": { 53 | "Type": "AWS::IAM::Policy", 54 | "Properties": { 55 | "PolicyName": "AppSyncCognitoAccess", 56 | "PolicyDocument": { 57 | "Version": "2012-10-17", 58 | "Statement": [ 59 | { 60 | "Effect": "Allow", 61 | "Action": "cognito-idp:*", 62 | "Resource": "*" 63 | } 64 | ] 65 | }, 66 | "Roles": [ 67 | { 68 | "Ref": "awsAppSyncServiceRole" 69 | } 70 | ] 71 | } 72 | }, 73 | "CognitoHTTPDataSource": { 74 | "Type": "AWS::AppSync::DataSource", 75 | "Properties": { 76 | "ApiId": { 77 | "Ref": "AppSyncApiId" 78 | }, 79 | "Name": "CognitoResolver", 80 | "Description": "Cognito HTTP data source", 81 | "Type": "HTTP", 82 | "HttpConfig": { 83 | "AuthorizationConfig": { 84 | "AuthorizationType": "AWS_IAM", 85 | "AwsIamConfig": { 86 | "SigningRegion": "ap-southeast-2", 87 | "SigningServiceName": "cognito-idp" 88 | } 89 | }, 90 | "Endpoint": "https://cognito-idp.ap-southeast-2.amazonaws.com/" 91 | }, 92 | "ServiceRoleArn": { 93 | "Fn::GetAtt": [ 94 | "awsAppSyncServiceRole", 95 | "Arn" 96 | ] 97 | } 98 | } 99 | }, 100 | "queryListUsersResolver": { 101 | "Type": "AWS::AppSync::Resolver", 102 | "Properties": { 103 | "ApiId": { 104 | "Ref": "AppSyncApiId" 105 | }, 106 | "TypeName": "Query", 107 | "FieldName": "listUsers", 108 | "DataSourceName": { 109 | "Fn::GetAtt": [ 110 | "CognitoHTTPDataSource", 111 | "Name" 112 | ] 113 | }, 114 | "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n \"resourcePath\": \"/\",\n \"params\":\n {\n \"headers\" : {\n \"Content-Type\" : \"application/x-amz-json-1.1\",\n \"X-Amz-Target\" : \"AWSCognitoIdentityProviderService.ListUsers\"\n },\n \"body\": {\n \"UserPoolId\":\"$context.arguments.UserPoolId\"\n }\n }\n}\n", 115 | "ResponseMappingTemplate": "$util.toJson($context.result.body)\n" 116 | } 117 | }, 118 | "queryListGroupsResolver": { 119 | "Type": "AWS::AppSync::Resolver", 120 | "Properties": { 121 | "ApiId": { 122 | "Ref": "AppSyncApiId" 123 | }, 124 | "TypeName": "Query", 125 | "FieldName": "listGroups", 126 | "DataSourceName": { 127 | "Fn::GetAtt": [ 128 | "CognitoHTTPDataSource", 129 | "Name" 130 | ] 131 | }, 132 | "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n \"resourcePath\": \"/\",\n \"params\":\n {\n \"headers\" : {\n \"Content-Type\" : \"application/x-amz-json-1.1\",\n \"X-Amz-Target\" : \"AWSCognitoIdentityProviderService.ListGroups\"\n },\n \"body\": {\n \"UserPoolId\":\"$context.arguments.UserPoolId\"\n }\n }\n}\n", 133 | "ResponseMappingTemplate": "$util.toJson($context.result.body)\n" 134 | } 135 | }, 136 | "queryListGroupMembersResolver": { 137 | "Type": "AWS::AppSync::Resolver", 138 | "Properties": { 139 | "ApiId": { 140 | "Ref": "AppSyncApiId" 141 | }, 142 | "TypeName": "Query", 143 | "FieldName": "listGroupMembers", 144 | "DataSourceName": { 145 | "Fn::GetAtt": [ 146 | "CognitoHTTPDataSource", 147 | "Name" 148 | ] 149 | }, 150 | "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n \"resourcePath\": \"/\",\n \"params\":\n {\n \"headers\" : {\n \"Content-Type\" : \"application/x-amz-json-1.1\",\n \"X-Amz-Target\" : \"AWSCognitoIdentityProviderService.ListUsersInGroup\"\n },\n \"body\": {\n \"UserPoolId\":\"$context.arguments.UserPoolId\",\n \"GroupName\":\"$context.arguments.GroupName\"\n }\n }\n}\n", 151 | "ResponseMappingTemplate": "$util.toJson($context.result.body)\n" 152 | } 153 | }, 154 | "mutationDeleteUserResolver": { 155 | "Type": "AWS::AppSync::Resolver", 156 | "Properties": { 157 | "ApiId": { 158 | "Ref": "AppSyncApiId" 159 | }, 160 | "TypeName": "Mutation", 161 | "FieldName": "deleteUser", 162 | "DataSourceName": { 163 | "Fn::GetAtt": [ 164 | "CognitoHTTPDataSource", 165 | "Name" 166 | ] 167 | }, 168 | "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n \"resourcePath\": \"/\",\n \"params\":\n {\n \"headers\" : {\n \"Content-Type\" : \"application/x-amz-json-1.1\",\n \"X-Amz-Target\" : \"AWSCognitoIdentityProviderService.AdminDeleteUser\"\n },\n \"body\": {\n \"UserPoolId\":\"$context.arguments.UserPoolId\",\n \"Username\":\"$context.arguments.Username\"\n }\n }\n}\n", 169 | "ResponseMappingTemplate": "$util.toJson($context.result.body)\n" 170 | } 171 | }, 172 | "mutationAddUserToGroupResolver": { 173 | "Type": "AWS::AppSync::Resolver", 174 | "Properties": { 175 | "ApiId": { 176 | "Ref": "AppSyncApiId" 177 | }, 178 | "TypeName": "Mutation", 179 | "FieldName": "addUserToGroup", 180 | "DataSourceName": { 181 | "Fn::GetAtt": [ 182 | "CognitoHTTPDataSource", 183 | "Name" 184 | ] 185 | }, 186 | "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n \"resourcePath\": \"/\",\n \"params\":\n {\n \"headers\" : {\n \"Content-Type\" : \"application/x-amz-json-1.1\",\n \"X-Amz-Target\" : \"AWSCognitoIdentityProviderService.AdminAddUserToGroup\"\n },\n \"body\": {\n \"UserPoolId\":\"$context.arguments.UserPoolId\",\n \"Username\":\"$context.arguments.Username\",\n \"Username\":\"$context.arguments.GroupName\"\n }\n }\n}\n", 187 | "ResponseMappingTemplate": "$util.toJson($context.result.body)\n" 188 | } 189 | }, 190 | "mutationAddGroupResolver": { 191 | "Type": "AWS::AppSync::Resolver", 192 | "Properties": { 193 | "ApiId": { 194 | "Ref": "AppSyncApiId" 195 | }, 196 | "TypeName": "Mutation", 197 | "FieldName": "addGroup", 198 | "DataSourceName": { 199 | "Fn::GetAtt": [ 200 | "CognitoHTTPDataSource", 201 | "Name" 202 | ] 203 | }, 204 | "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n \"resourcePath\": \"/\",\n \"params\":\n {\n \"headers\" : {\n \"Content-Type\" : \"application/x-amz-json-1.1\",\n \"X-Amz-Target\" : \"AWSCognitoIdentityProviderService.CreateGroup\"\n },\n \"body\": {\n \"UserPoolId\":\"$context.arguments.UserPoolId\",\n \"GroupName\":\"$context.arguments.GroupName\"\n }\n }\n}\n", 205 | "ResponseMappingTemplate": "$util.toJson($context.result.body)\n" 206 | } 207 | }, 208 | "mutationDeleteGroupResolver": { 209 | "Type": "AWS::AppSync::Resolver", 210 | "Properties": { 211 | "ApiId": { 212 | "Ref": "AppSyncApiId" 213 | }, 214 | "TypeName": "Mutation", 215 | "FieldName": "deleteGroup", 216 | "DataSourceName": { 217 | "Fn::GetAtt": [ 218 | "CognitoHTTPDataSource", 219 | "Name" 220 | ] 221 | }, 222 | "RequestMappingTemplate": "{\n \"version\": \"2018-05-29\",\n \"method\": \"POST\",\n \"resourcePath\": \"/\",\n \"params\":\n {\n \"headers\" : {\n \"Content-Type\" : \"application/x-amz-json-1.1\",\n \"X-Amz-Target\" : \"AWSCognitoIdentityProviderService.DeleteGroup\"\n },\n \"body\": {\n \"UserPoolId\":\"$context.arguments.UserPoolId\",\n \"GroupName\":\"$context.arguments.GroupName\"\n }\n }\n}\n", 223 | "ResponseMappingTemplate": "$util.toJson($context.result.body)\n" 224 | } 225 | } 226 | }, 227 | "Conditions": { 228 | "HasEnvironmentParameter": { 229 | "Fn::Not": [ 230 | { 231 | "Fn::Equals": [ 232 | { 233 | "Ref": "env" 234 | }, 235 | "NONE" 236 | ] 237 | } 238 | ] 239 | }, 240 | "AlwaysFalse": { 241 | "Fn::Equals": [ 242 | "true", 243 | "false" 244 | ] 245 | } 246 | }, 247 | "Outputs": { 248 | "CognitoHTTPDataSourceARN": { 249 | "Description": "Cognito Data Source ARN", 250 | "Value": { 251 | "Ref": "CognitoHTTPDataSource" 252 | } 253 | }, 254 | "queryListUsersResolverARN": { 255 | "Description": "ListUsers Resolver ARN", 256 | "Value": { 257 | "Ref": "queryListUsersResolver" 258 | } 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /amplify/backend/auth/surveypwa1a7615c6/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "identityPoolName": "surveypwa1a7615c6_identitypool_1a7615c6", 3 | "allowUnauthenticatedIdentities": false, 4 | "openIdLambdaRoleName": "survey_1a7615c6_openid_lambda_role", 5 | "resourceNameTruncated": "surveya1504313", 6 | "userPoolName": "surveypwa1a7615c6_userpool_1a7615c6", 7 | "autoVerifiedAttributes": [ 8 | "email" 9 | ], 10 | "mfaConfiguration": "OFF", 11 | "mfaTypes": [ 12 | "SMS Text Message" 13 | ], 14 | "smsAuthenticationMessage": "Your authentication code is {####}", 15 | "smsVerificationMessage": "Your verification code is {####}", 16 | "emailVerificationSubject": "Your verification code", 17 | "emailVerificationMessage": "Your verification code is {####}", 18 | "defaultPasswordPolicy": false, 19 | "passwordPolicyMinLength": 8, 20 | "passwordPolicyCharacters": [], 21 | "requiredAttributes": [ 22 | "email" 23 | ], 24 | "userpoolClientName": "survey1a7615c6_app_client", 25 | "userpoolClientGenerateSecret": true, 26 | "userpoolClientRefreshTokenValidity": 30, 27 | "userpoolClientWriteAttributes": [ 28 | "email" 29 | ], 30 | "userpoolClientReadAttributes": [ 31 | "email" 32 | ], 33 | "mfaLambdaRole": "survey1a7615c6_totp_lambda_role", 34 | "userpoolClientLambdaRole": "survey1a7615c6_userpoolclient_lambda_role", 35 | "userpoolClientSetAttributes": false, 36 | "lambdaLogPolicy": "survey_1a7615c6_lambda_log_policy", 37 | "openIdRolePolicy": "survey_1a7615c6_openid_pass_role_policy", 38 | "openIdLambdaIAMPolicy": "survey_1a7615c6_openid_lambda_iam_policy", 39 | "openIdLogPolicy": "survey_1a7615c6_openid_lambda_log_policy", 40 | "roleName": "survey1a7615c6_sns-role", 41 | "roleExternalId": "survey1a7615c6_role_external_id", 42 | "policyName": "survey1a7615c6-sns-policy", 43 | "mfaLambdaLogPolicy": "survey1a7615c6_totp_lambda_log_policy", 44 | "mfaPassRolePolicy": "survey1a7615c6_totp_pass_role_policy", 45 | "mfaLambdaIAMPolicy": "survey1a7615c6_totp_lambda_iam_policy", 46 | "userpoolClientLogPolicy": "survey1a7615c6_userpoolclient_lambda_log_policy", 47 | "userpoolClientLambdaPolicy": "survey1a7615c6_userpoolclient_lambda_iam_policy", 48 | "resourceName": "surveypwa1a7615c6", 49 | "authSelections": "identityPoolAndUserPool", 50 | "authRoleName": { 51 | "Ref": "AuthRoleName" 52 | }, 53 | "unauthRoleName": { 54 | "Ref": "UnauthRoleName" 55 | }, 56 | "authRoleArn": { 57 | "Fn::GetAtt": [ 58 | "AuthRole", 59 | "Arn" 60 | ] 61 | }, 62 | "unauthRoleArn": { 63 | "Fn::GetAtt": [ 64 | "UnauthRole", 65 | "Arn" 66 | ] 67 | }, 68 | "useDefault": "manual", 69 | "thirdPartyAuth": false, 70 | "triggers": "{\"PostConfirmation\":[\"add-to-group\"]}", 71 | "hostedUI": false, 72 | "PostConfirmation": "surveypwa1a7615c6PostConfirmation", 73 | "parentStack": { 74 | "Ref": "AWS::StackId" 75 | }, 76 | "permissions": [ 77 | "{\"policyName\":\"AddToGroupCognito\",\"trigger\":\"PostConfirmation\",\"effect\":\"Allow\",\"actions\":[\"cognito-idp:AdminAddUserToGroup\",\"cognito-idp:GetGroup\",\"cognito-idp:CreateGroup\"],\"resource\":{\"paramType\":\"!GetAtt\",\"keys\":[\"UserPool\",\"Arn\"]}}" 78 | ], 79 | "dependsOn": [ 80 | { 81 | "category": "function", 82 | "resourceName": "surveypwa1a7615c6PostConfirmation", 83 | "triggerProvider": "Cognito", 84 | "attributes": [ 85 | "Arn", 86 | "Name" 87 | ] 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /amplify/backend/auth/surveypwa1a7615c6/surveypwa1a7615c6-cloudformation-template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Parameters: 4 | env: 5 | Type: String 6 | authRoleName: 7 | Type: String 8 | unauthRoleName: 9 | Type: String 10 | authRoleArn: 11 | Type: String 12 | unauthRoleArn: 13 | Type: String 14 | 15 | 16 | 17 | 18 | 19 | 20 | functionsurveypwa1a7615c6PostConfirmationArn: 21 | Type: String 22 | Default: functionsurveypwa1a7615c6PostConfirmationArn 23 | 24 | functionsurveypwa1a7615c6PostConfirmationName: 25 | Type: String 26 | Default: functionsurveypwa1a7615c6PostConfirmationName 27 | 28 | 29 | 30 | 31 | 32 | identityPoolName: 33 | Type: String 34 | 35 | allowUnauthenticatedIdentities: 36 | Type: String 37 | 38 | openIdLambdaRoleName: 39 | Type: String 40 | 41 | resourceNameTruncated: 42 | Type: String 43 | 44 | userPoolName: 45 | Type: String 46 | 47 | autoVerifiedAttributes: 48 | Type: CommaDelimitedList 49 | 50 | mfaConfiguration: 51 | Type: String 52 | 53 | mfaTypes: 54 | Type: CommaDelimitedList 55 | 56 | smsAuthenticationMessage: 57 | Type: String 58 | 59 | smsVerificationMessage: 60 | Type: String 61 | 62 | emailVerificationSubject: 63 | Type: String 64 | 65 | emailVerificationMessage: 66 | Type: String 67 | 68 | defaultPasswordPolicy: 69 | Type: String 70 | 71 | passwordPolicyMinLength: 72 | Type: Number 73 | 74 | passwordPolicyCharacters: 75 | Type: CommaDelimitedList 76 | 77 | requiredAttributes: 78 | Type: CommaDelimitedList 79 | 80 | userpoolClientName: 81 | Type: String 82 | 83 | userpoolClientGenerateSecret: 84 | Type: String 85 | 86 | userpoolClientRefreshTokenValidity: 87 | Type: Number 88 | 89 | userpoolClientWriteAttributes: 90 | Type: CommaDelimitedList 91 | 92 | userpoolClientReadAttributes: 93 | Type: CommaDelimitedList 94 | 95 | mfaLambdaRole: 96 | Type: String 97 | 98 | userpoolClientLambdaRole: 99 | Type: String 100 | 101 | userpoolClientSetAttributes: 102 | Type: String 103 | 104 | lambdaLogPolicy: 105 | Type: String 106 | 107 | openIdRolePolicy: 108 | Type: String 109 | 110 | openIdLambdaIAMPolicy: 111 | Type: String 112 | 113 | openIdLogPolicy: 114 | Type: String 115 | 116 | roleName: 117 | Type: String 118 | 119 | roleExternalId: 120 | Type: String 121 | 122 | policyName: 123 | Type: String 124 | 125 | mfaLambdaLogPolicy: 126 | Type: String 127 | 128 | mfaPassRolePolicy: 129 | Type: String 130 | 131 | mfaLambdaIAMPolicy: 132 | Type: String 133 | 134 | userpoolClientLogPolicy: 135 | Type: String 136 | 137 | userpoolClientLambdaPolicy: 138 | Type: String 139 | 140 | resourceName: 141 | Type: String 142 | 143 | authSelections: 144 | Type: String 145 | 146 | useDefault: 147 | Type: String 148 | 149 | thirdPartyAuth: 150 | Type: String 151 | 152 | triggers: 153 | Type: String 154 | 155 | hostedUI: 156 | Type: String 157 | 158 | PostConfirmation: 159 | Type: String 160 | 161 | parentStack: 162 | Type: String 163 | 164 | permissions: 165 | Type: CommaDelimitedList 166 | 167 | dependsOn: 168 | Type: CommaDelimitedList 169 | 170 | Conditions: 171 | ShouldNotCreateEnvResources: !Equals [ !Ref env, NONE ] 172 | 173 | Resources: 174 | 175 | 176 | # BEGIN SNS ROLE RESOURCE 177 | SNSRole: 178 | # Created to allow the UserPool SMS Config to publish via the Simple Notification Service during MFA Process 179 | Type: AWS::IAM::Role 180 | Properties: 181 | RoleName: !If [ShouldNotCreateEnvResources, 'surveya1504313_sns-role', !Join ['',['surveya1504313_sns-role', '-', !Ref env]]] 182 | AssumeRolePolicyDocument: 183 | Version: "2012-10-17" 184 | Statement: 185 | - Sid: "" 186 | Effect: "Allow" 187 | Principal: 188 | Service: "cognito-idp.amazonaws.com" 189 | Action: 190 | - "sts:AssumeRole" 191 | Condition: 192 | StringEquals: 193 | sts:ExternalId: surveya1504313_role_external_id 194 | Policies: 195 | - 196 | PolicyName: surveya1504313-sns-policy 197 | PolicyDocument: 198 | Version: "2012-10-17" 199 | Statement: 200 | - 201 | Effect: "Allow" 202 | Action: 203 | - "sns:Publish" 204 | Resource: "*" 205 | # BEGIN USER POOL RESOURCES 206 | UserPool: 207 | # Created upon user selection 208 | # Depends on SNS Role for Arn if MFA is enabled 209 | Type: AWS::Cognito::UserPool 210 | UpdateReplacePolicy: Retain 211 | Properties: 212 | UserPoolName: !If [ShouldNotCreateEnvResources, !Ref userPoolName, !Join ['',[!Ref userPoolName, '-', !Ref env]]] 213 | 214 | Schema: 215 | 216 | - 217 | Name: email 218 | Required: true 219 | Mutable: true 220 | 221 | 222 | 223 | LambdaConfig: 224 | 225 | 226 | 227 | 228 | 229 | PostConfirmation: !Ref functionsurveypwa1a7615c6PostConfirmationArn 230 | 231 | 232 | 233 | 234 | 235 | 236 | AutoVerifiedAttributes: !Ref autoVerifiedAttributes 237 | 238 | 239 | EmailVerificationMessage: !Ref emailVerificationMessage 240 | EmailVerificationSubject: !Ref emailVerificationSubject 241 | 242 | Policies: 243 | PasswordPolicy: 244 | MinimumLength: !Ref passwordPolicyMinLength 245 | RequireLowercase: false 246 | RequireNumbers: false 247 | RequireSymbols: false 248 | RequireUppercase: false 249 | 250 | MfaConfiguration: !Ref mfaConfiguration 251 | SmsVerificationMessage: !Ref smsVerificationMessage 252 | SmsConfiguration: 253 | SnsCallerArn: !GetAtt SNSRole.Arn 254 | ExternalId: surveya1504313_role_external_id 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | UserPoolPostConfirmationLambdaInvokePermission: 263 | Type: "AWS::Lambda::Permission" 264 | DependsOn: UserPool 265 | Properties: 266 | Action: "lambda:invokeFunction" 267 | Principal: "cognito-idp.amazonaws.com" 268 | FunctionName: !Ref functionsurveypwa1a7615c6PostConfirmationName 269 | SourceArn: !GetAtt UserPool.Arn 270 | 271 | 272 | 273 | 274 | # Updating lambda role with permissions to Cognito 275 | 276 | 277 | surveypwa1a7615c6PostConfirmationAddToGroupCognito: 278 | Type: AWS::IAM::Policy 279 | Properties: 280 | PolicyName: surveypwa1a7615c6PostConfirmationAddToGroupCognito 281 | PolicyDocument: 282 | Version: '2012-10-17' 283 | Statement: 284 | - Effect: Allow 285 | Action: 286 | 287 | - cognito-idp:AdminAddUserToGroup 288 | 289 | - cognito-idp:GetGroup 290 | 291 | - cognito-idp:CreateGroup 292 | 293 | - cognito-idp:ListGroups 294 | 295 | 296 | 297 | Resource: !GetAtt 298 | 299 | - UserPool 300 | 301 | - Arn 302 | 303 | 304 | 305 | 306 | Roles: 307 | - !Join ['',["surveypwa1a7615c6PostConfirmation", '-', !Ref env]] 308 | 309 | 310 | 311 | UserPoolClientWeb: 312 | # Created provide application access to user pool 313 | # Depends on UserPool for ID reference 314 | Type: "AWS::Cognito::UserPoolClient" 315 | Properties: 316 | ClientName: survey1a7615c6_app_clientWeb 317 | 318 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 319 | UserPoolId: !Ref UserPool 320 | DependsOn: UserPool 321 | UserPoolClient: 322 | # Created provide application access to user pool 323 | # Depends on UserPool for ID reference 324 | Type: "AWS::Cognito::UserPoolClient" 325 | Properties: 326 | ClientName: !Ref userpoolClientName 327 | 328 | GenerateSecret: !Ref userpoolClientGenerateSecret 329 | RefreshTokenValidity: !Ref userpoolClientRefreshTokenValidity 330 | UserPoolId: !Ref UserPool 331 | DependsOn: UserPool 332 | # BEGIN USER POOL LAMBDA RESOURCES 333 | UserPoolClientRole: 334 | # Created to execute Lambda which gets userpool app client config values 335 | Type: 'AWS::IAM::Role' 336 | Properties: 337 | RoleName: !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',[!Ref userpoolClientLambdaRole, '-', !Ref env]]] 338 | AssumeRolePolicyDocument: 339 | Version: '2012-10-17' 340 | Statement: 341 | - Effect: Allow 342 | Principal: 343 | Service: 344 | - lambda.amazonaws.com 345 | Action: 346 | - 'sts:AssumeRole' 347 | DependsOn: UserPoolClient 348 | UserPoolClientLambda: 349 | # Lambda which gets userpool app client config values 350 | # Depends on UserPool for id 351 | # Depends on UserPoolClientRole for role ARN 352 | Type: 'AWS::Lambda::Function' 353 | Properties: 354 | Code: 355 | ZipFile: !Join 356 | - |+ 357 | - - 'const response = require(''cfn-response'');' 358 | - 'const aws = require(''aws-sdk'');' 359 | - 'const identity = new aws.CognitoIdentityServiceProvider();' 360 | - 'exports.handler = (event, context, callback) => {' 361 | - ' if (event.RequestType == ''Delete'') { ' 362 | - ' response.send(event, context, response.SUCCESS, {})' 363 | - ' }' 364 | - ' if (event.RequestType == ''Update'' || event.RequestType == ''Create'') {' 365 | - ' const params = {' 366 | - ' ClientId: event.ResourceProperties.clientId,' 367 | - ' UserPoolId: event.ResourceProperties.userpoolId' 368 | - ' };' 369 | - ' identity.describeUserPoolClient(params).promise()' 370 | - ' .then((res) => {' 371 | - ' response.send(event, context, response.SUCCESS, {''appSecret'': res.UserPoolClient.ClientSecret});' 372 | - ' })' 373 | - ' .catch((err) => {' 374 | - ' response.send(event, context, response.FAILED, {err});' 375 | - ' });' 376 | - ' }' 377 | - '};' 378 | Handler: index.handler 379 | Runtime: nodejs8.10 380 | Timeout: '300' 381 | Role: !GetAtt 382 | - UserPoolClientRole 383 | - Arn 384 | DependsOn: UserPoolClientRole 385 | UserPoolClientLambdaPolicy: 386 | # Sets userpool policy for the role that executes the Userpool Client Lambda 387 | # Depends on UserPool for Arn 388 | # Marked as depending on UserPoolClientRole for easier to understand CFN sequencing 389 | Type: 'AWS::IAM::Policy' 390 | Properties: 391 | PolicyName: surveya1504313_userpoolclient_lambda_iam_policy 392 | Roles: 393 | - !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',[!Ref userpoolClientLambdaRole, '-', !Ref env]]] 394 | PolicyDocument: 395 | Version: '2012-10-17' 396 | Statement: 397 | - Effect: Allow 398 | Action: 399 | - 'cognito-idp:DescribeUserPoolClient' 400 | Resource: !GetAtt UserPool.Arn 401 | DependsOn: UserPoolClientLambda 402 | UserPoolClientLogPolicy: 403 | # Sets log policy for the role that executes the Userpool Client Lambda 404 | # Depends on UserPool for Arn 405 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 406 | Type: 'AWS::IAM::Policy' 407 | Properties: 408 | PolicyName: surveya1504313_userpoolclient_lambda_log_policy 409 | Roles: 410 | - !If [ShouldNotCreateEnvResources, !Ref userpoolClientLambdaRole, !Join ['',[!Ref userpoolClientLambdaRole, '-', !Ref env]]] 411 | PolicyDocument: 412 | Version: 2012-10-17 413 | Statement: 414 | - Effect: Allow 415 | Action: 416 | - 'logs:CreateLogGroup' 417 | - 'logs:CreateLogStream' 418 | - 'logs:PutLogEvents' 419 | Resource: !Sub 420 | - arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:* 421 | - { region: !Ref "AWS::Region", account: !Ref "AWS::AccountId", lambda: !Ref UserPoolClientLambda} 422 | DependsOn: UserPoolClientLambdaPolicy 423 | UserPoolClientInputs: 424 | # Values passed to Userpool client Lambda 425 | # Depends on UserPool for Id 426 | # Depends on UserPoolClient for Id 427 | # Marked as depending on UserPoolClientLambdaPolicy for easier to understand CFN sequencing 428 | Type: 'Custom::LambdaCallout' 429 | Properties: 430 | ServiceToken: !GetAtt UserPoolClientLambda.Arn 431 | clientId: !Ref UserPoolClient 432 | userpoolId: !Ref UserPool 433 | DependsOn: UserPoolClientLogPolicy 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | # BEGIN IDENTITY POOL RESOURCES 442 | 443 | 444 | IdentityPool: 445 | # Always created 446 | Type: AWS::Cognito::IdentityPool 447 | Properties: 448 | IdentityPoolName: !If [ShouldNotCreateEnvResources, 'surveypwa1a7615c6_identitypool_1a7615c6', !Join ['',['surveypwa1a7615c6_identitypool_1a7615c6', '__', !Ref env]]] 449 | 450 | CognitoIdentityProviders: 451 | - ClientId: !Ref UserPoolClient 452 | ProviderName: !Sub 453 | - cognito-idp.${region}.amazonaws.com/${client} 454 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 455 | - ClientId: !Ref UserPoolClientWeb 456 | ProviderName: !Sub 457 | - cognito-idp.${region}.amazonaws.com/${client} 458 | - { region: !Ref "AWS::Region", client: !Ref UserPool} 459 | 460 | AllowUnauthenticatedIdentities: !Ref allowUnauthenticatedIdentities 461 | 462 | 463 | DependsOn: UserPoolClientInputs 464 | 465 | 466 | IdentityPoolRoleMap: 467 | # Created to map Auth and Unauth roles to the identity pool 468 | # Depends on Identity Pool for ID ref 469 | Type: AWS::Cognito::IdentityPoolRoleAttachment 470 | Properties: 471 | IdentityPoolId: !Ref IdentityPool 472 | Roles: 473 | unauthenticated: !Ref unauthRoleArn 474 | authenticated: !Ref authRoleArn 475 | DependsOn: IdentityPool 476 | 477 | 478 | Outputs : 479 | 480 | IdentityPoolId: 481 | Value: !Ref 'IdentityPool' 482 | Description: Id for the identity pool 483 | IdentityPoolName: 484 | Value: !GetAtt IdentityPool.Name 485 | 486 | 487 | 488 | 489 | UserPoolId: 490 | Value: !Ref 'UserPool' 491 | Description: Id for the user pool 492 | UserPoolName: 493 | Value: !Ref userPoolName 494 | AppClientIDWeb: 495 | Value: !Ref 'UserPoolClientWeb' 496 | Description: The user pool app client id for web 497 | AppClientID: 498 | Value: !Ref 'UserPoolClient' 499 | Description: The user pool app client id 500 | AppClientSecret: 501 | Value: !GetAtt UserPoolClientInputs.appSecret 502 | 503 | 504 | 505 | 506 | 507 | 508 | -------------------------------------------------------------------------------- /amplify/backend/backend-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "surveypwa1a7615c6": { 4 | "service": "Cognito", 5 | "providerPlugin": "awscloudformation", 6 | "dependsOn": [ 7 | { 8 | "category": "function", 9 | "resourceName": "surveypwa1a7615c6PostConfirmation", 10 | "triggerProvider": "Cognito", 11 | "attributes": [ 12 | "Arn", 13 | "Name" 14 | ] 15 | } 16 | ] 17 | } 18 | }, 19 | "analytics": { 20 | "surveypwa": { 21 | "service": "Pinpoint", 22 | "providerPlugin": "awscloudformation" 23 | } 24 | }, 25 | "api": { 26 | "surveypwa": { 27 | "service": "AppSync", 28 | "providerPlugin": "awscloudformation", 29 | "output": { 30 | "securityType": "AMAZON_COGNITO_USER_POOLS" 31 | } 32 | } 33 | }, 34 | "function": { 35 | "surveypwa1a7615c6PostConfirmation": { 36 | "service": "Lambda", 37 | "providerPlugin": "awscloudformation", 38 | "build": true 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/function-parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "trigger": true, 3 | "modules": [ 4 | "add-to-group" 5 | ], 6 | "parentResource": "surveypwa1a7615c6", 7 | "functionName": "surveypwa1a7615c6PostConfirmation", 8 | "parentStack": "auth", 9 | "triggerEnvs": "[]", 10 | "triggerTemplate": "PostConfirmation.json.ejs", 11 | "roleName": "surveypwa1a7615c6PostConfirmation", 12 | "skipEdit": true, 13 | "resourceName": "surveypwa1a7615c6PostConfirmation" 14 | } -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": "add-to-group", 3 | "resourceName": "surveypwa1a7615c6PostConfirmation" 4 | } -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/src/add-to-group.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable-line */ const aws = require('aws-sdk'); 2 | 3 | exports.handler = async (event, context, callback) => { 4 | const cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' }); 5 | 6 | const listGroupsParams = { 7 | UserPoolId: event.userPoolId 8 | } 9 | 10 | var adminUser = false 11 | 12 | await cognitoidentityserviceprovider.listGroups(listGroupsParams, async (err, data) => { 13 | if (err) console.log(err, err.stack); 14 | else{ 15 | console.log("Number of Groups in Pool: " + data.Groups.length); 16 | if(data.Groups.length === 0) { 17 | // This is the first time a user has registered, so create groups and set user as Admin 18 | adminUser = true 19 | const adminGroupParams = { 20 | GroupName: "SurveyAdmins", 21 | UserPoolId: event.userPoolId, 22 | }; 23 | const usersGroupParams = { 24 | GroupName: "Users", 25 | UserPoolId: event.userPoolId, 26 | }; 27 | await cognitoidentityserviceprovider.createGroup(adminGroupParams).promise(); 28 | await cognitoidentityserviceprovider.createGroup(usersGroupParams).promise(); 29 | } 30 | } 31 | }).promise(); 32 | 33 | 34 | 35 | const addUserParams = { 36 | GroupName: adminUser ? "SurveyAdmins" : "Users", 37 | UserPoolId: event.userPoolId, 38 | Username: event.userName, 39 | }; 40 | 41 | cognitoidentityserviceprovider.adminAddUserToGroup(addUserParams, (err) => { 42 | if (err) { 43 | callback(err); 44 | } 45 | callback(null, event); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/src/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "userPoolId": "testID", 4 | "userName": "testUser" 5 | }, 6 | "response": {} 7 | } -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | this file will loop through all js modules which are uploaded to the lambda resource, 3 | provided that the file names (without extension) are included in the "MODULES" env variable. 4 | "MODULES" is a comma-delimmited string. 5 | */ 6 | 7 | exports.handler = (event, context, callback) => { 8 | const modules = process.env.MODULES.split(','); 9 | for (let i = 0; i < modules.length; i += 1) { 10 | const { handler } = require(modules[i]); 11 | handler(event, context, callback); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surveypwa1a7615c6PostConfirmation", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.19.0", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", 10 | "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", 11 | "requires": { 12 | "follow-redirects": "1.5.10", 13 | "is-buffer": "^2.0.2" 14 | } 15 | }, 16 | "debug": { 17 | "version": "3.1.0", 18 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 19 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 20 | "requires": { 21 | "ms": "2.0.0" 22 | } 23 | }, 24 | "follow-redirects": { 25 | "version": "1.5.10", 26 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 27 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 28 | "requires": { 29 | "debug": "=3.1.0" 30 | } 31 | }, 32 | "is-buffer": { 33 | "version": "2.0.3", 34 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", 35 | "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" 36 | }, 37 | "ms": { 38 | "version": "2.0.0", 39 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 40 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surveypwa1a7615c6PostConfirmation", 3 | "version": "2.0.0", 4 | "description": "Lambda function generated by Amplify", 5 | "main": "index.js", 6 | "license": "Apache-2.0", 7 | "dependencies": { 8 | "axios": "latest" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /amplify/backend/function/surveypwa1a7615c6PostConfirmation/surveypwa1a7615c6PostConfirmation-cloudformation-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Lambda resource stack creation using Amplify CLI", 4 | "Parameters": { 5 | "GROUP": { 6 | "Type": "String", 7 | "Default": "" 8 | }, 9 | "modules": { 10 | "Type": "String", 11 | "Default": "", 12 | "Description": "Comma-delimmited list of modules to be executed by a lambda trigger. Sent to resource as an env variable." 13 | }, 14 | "resourceName": { 15 | "Type": "String", 16 | "Default": "" 17 | }, 18 | "trigger": { 19 | "Type": "String", 20 | "Default": "true" 21 | }, 22 | "functionName": { 23 | "Type": "String", 24 | "Default": "" 25 | }, 26 | "roleName": { 27 | "Type": "String", 28 | "Default": "" 29 | }, 30 | "parentResource": { 31 | "Type": "String", 32 | "Default": "" 33 | }, 34 | "parentStack": { 35 | "Type": "String", 36 | "Default": "" 37 | }, 38 | "env": { 39 | "Type": "String" 40 | } 41 | }, 42 | "Conditions": { 43 | "ShouldNotCreateEnvResources": { 44 | "Fn::Equals": [ 45 | { 46 | "Ref": "env" 47 | }, 48 | "NONE" 49 | ] 50 | } 51 | }, 52 | "Resources": { 53 | "LambdaFunction": { 54 | "Type": "AWS::Lambda::Function", 55 | "Metadata": { 56 | "aws:asset:path": "./src", 57 | "aws:asset:property": "Code" 58 | }, 59 | "Properties": { 60 | "Handler": "index.handler", 61 | "FunctionName": { 62 | "Fn::If": [ 63 | "ShouldNotCreateEnvResources", 64 | "surveypwa1a7615c6PostConfirmation", 65 | { 66 | "Fn::Join": [ 67 | "", 68 | [ 69 | "surveypwa1a7615c6PostConfirmation", 70 | "-", 71 | { 72 | "Ref": "env" 73 | } 74 | ] 75 | ] 76 | } 77 | ] 78 | }, 79 | "Environment": { 80 | "Variables": { 81 | "ENV": { 82 | "Ref": "env" 83 | }, 84 | "MODULES": { 85 | "Ref": "modules" 86 | }, 87 | "REGION": { 88 | "Ref": "AWS::Region" 89 | }, 90 | "GROUP": { 91 | "Ref": "GROUP" 92 | } 93 | } 94 | }, 95 | "Role": { 96 | "Fn::GetAtt": [ 97 | "LambdaExecutionRole", 98 | "Arn" 99 | ] 100 | }, 101 | "Runtime": "nodejs8.10", 102 | "Timeout": "25", 103 | "Code": { 104 | "S3Bucket": "surveypwa-dev-20190605134212-deployment", 105 | "S3Key": "amplify-builds/surveypwa1a7615c6PostConfirmation-352b4c326e6135635756-build.zip" 106 | } 107 | } 108 | }, 109 | "LambdaExecutionRole": { 110 | "Type": "AWS::IAM::Role", 111 | "Properties": { 112 | "RoleName": { 113 | "Fn::If": [ 114 | "ShouldNotCreateEnvResources", 115 | "surveypwa1a7615c6PostConfirmation", 116 | { 117 | "Fn::Join": [ 118 | "", 119 | [ 120 | "surveypwa1a7615c6PostConfirmation", 121 | "-", 122 | { 123 | "Ref": "env" 124 | } 125 | ] 126 | ] 127 | } 128 | ] 129 | }, 130 | "AssumeRolePolicyDocument": { 131 | "Version": "2012-10-17", 132 | "Statement": [ 133 | { 134 | "Effect": "Allow", 135 | "Principal": { 136 | "Service": [ 137 | "lambda.amazonaws.com" 138 | ] 139 | }, 140 | "Action": [ 141 | "sts:AssumeRole" 142 | ] 143 | } 144 | ] 145 | } 146 | } 147 | }, 148 | "lambdaexecutionpolicy": { 149 | "DependsOn": [ 150 | "LambdaExecutionRole" 151 | ], 152 | "Type": "AWS::IAM::Policy", 153 | "Properties": { 154 | "PolicyName": "lambda-execution-policy", 155 | "Roles": [ 156 | { 157 | "Ref": "LambdaExecutionRole" 158 | } 159 | ], 160 | "PolicyDocument": { 161 | "Version": "2012-10-17", 162 | "Statement": [ 163 | { 164 | "Effect": "Allow", 165 | "Action": [ 166 | "logs:CreateLogGroup", 167 | "logs:CreateLogStream", 168 | "logs:PutLogEvents" 169 | ], 170 | "Resource": { 171 | "Fn::Sub": [ 172 | "arn:aws:logs:${region}:${account}:log-group:/aws/lambda/${lambda}:log-stream:*", 173 | { 174 | "region": { 175 | "Ref": "AWS::Region" 176 | }, 177 | "account": { 178 | "Ref": "AWS::AccountId" 179 | }, 180 | "lambda": { 181 | "Ref": "LambdaFunction" 182 | } 183 | } 184 | ] 185 | } 186 | } 187 | ] 188 | } 189 | } 190 | } 191 | }, 192 | "Outputs": { 193 | "Name": { 194 | "Value": { 195 | "Ref": "LambdaFunction" 196 | } 197 | }, 198 | "Arn": { 199 | "Value": { 200 | "Fn::GetAtt": [ 201 | "LambdaFunction", 202 | "Arn" 203 | ] 204 | } 205 | }, 206 | "Region": { 207 | "Value": { 208 | "Ref": "AWS::Region" 209 | } 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pwa", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@date-io/date-fns": "^1.3.6", 7 | "@date-io/moment": "^1.3.7", 8 | "@material-ui/core": "^4.0.0", 9 | "@material-ui/icons": "^4.0.0", 10 | "@material-ui/pickers": "^3.0.0", 11 | "apollo-boost": "^0.4.0", 12 | "apollo-link-state": "^0.4.2", 13 | "aws-amplify": "^1.1.28", 14 | "aws-amplify-react": "^2.3.8", 15 | "aws-appsync": "^1.8.0", 16 | "aws-sdk": "^2.494.0", 17 | "date-fns": "^2.0.0-alpha.27", 18 | "graphql": "^14.3.1", 19 | "graphql-tag": "^2.10.1", 20 | "moment": "^2.24.0", 21 | "react": "^16.8.6", 22 | "react-apollo": "^2.5.6", 23 | "react-big-calendar": "^0.21.0", 24 | "react-dom": "^16.8.6", 25 | "react-router": "^5.0.0", 26 | "react-router-dom": "^5.0.0", 27 | "react-scripts": "3.0.1", 28 | "uuid": "^3.3.2" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/Deck_Clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-survey-tool/2c66b0ff22ec6b7dc05f23ad59223f3425612b9f/public/Deck_Clock.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-survey-tool/2c66b0ff22ec6b7dc05f23ad59223f3425612b9f/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Survey Tool", 3 | "name": "Survey Tool", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "Deck_Clock.png", 12 | "type": "image/png", 13 | "sizes": "192x192 512x512" 14 | } 15 | ], 16 | "start_url": "/", 17 | "scope": "/", 18 | "display": "standalone", 19 | "theme_color": "#000000", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /public/simpsons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-survey-tool/2c66b0ff22ec6b7dc05f23ad59223f3425612b9f/public/simpsons.jpg -------------------------------------------------------------------------------- /src/assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-survey-tool/2c66b0ff22ec6b7dc05f23ad59223f3425612b9f/src/assets/header.png -------------------------------------------------------------------------------- /src/assets/surveytoolarchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-appsync-survey-tool/2c66b0ff22ec6b7dc05f23ad59223f3425612b9f/src/assets/surveytoolarchitecture.png -------------------------------------------------------------------------------- /src/components/addentry/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { graphql, compose, withApollo } from 'react-apollo'; 4 | import gql from 'graphql-tag'; 5 | 6 | import { v4 as uuid } from 'uuid'; 7 | 8 | import 'date-fns'; 9 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 10 | import Button from '@material-ui/core/Button'; 11 | import Container from '@material-ui/core/Container'; 12 | import Box from '@material-ui/core/Box'; 13 | import FormControl from '@material-ui/core/FormControl'; 14 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 15 | import FormLabel from '@material-ui/core/FormLabel'; 16 | import RadioGroup from '@material-ui/core/RadioGroup'; 17 | import Radio from '@material-ui/core/Radio'; 18 | import TextField from '@material-ui/core/TextField'; 19 | import AddIcon from '@material-ui/icons/Add'; 20 | import CircularProgress from '@material-ui/core/CircularProgress'; 21 | import Paper from '@material-ui/core/Paper'; 22 | import Typography from '@material-ui/core/Typography'; 23 | import Grid from '@material-ui/core/Grid'; 24 | import MomentUtils from '@date-io/moment'; 25 | import { 26 | MuiPickersUtilsProvider, 27 | KeyboardDateTimePicker, 28 | } from '@material-ui/pickers'; 29 | 30 | import { getQuestionnaire } from '../../graphql/queries'; 31 | import { createResponses } from '../../graphql/mutations'; 32 | import { createSurveyEntries } from '../../graphql/mutations'; 33 | 34 | const useStyles = makeStyles((theme) => 35 | createStyles({ 36 | button: { 37 | margin: 2, 38 | }, 39 | input: { 40 | display: 'none', 41 | }, 42 | formControl: { 43 | margin: 5, 44 | }, 45 | textField: { 46 | marginLeft: 1, 47 | marginRight: 1, 48 | }, 49 | grid: { 50 | width: '100%', 51 | }, 52 | date: { 53 | width: '50%', 54 | }, 55 | progress: { 56 | margin: 20, 57 | }, 58 | root: { 59 | padding: theme.spacing(3, 2), 60 | }, 61 | }) 62 | ) 63 | 64 | const AddEntryPart = (props) => { 65 | const classes = useStyles(); 66 | // eslint-disable-next-line 67 | const [value, setValue] = React.useState(''); 68 | const [radioValue, setRadioValue] = React.useState(''); 69 | const [responses, setResponses] = React.useState([]); 70 | const [selectedDate] = React.useState(Date.now()); 71 | const [group] = React.useState(uuid()); 72 | const [yesno] = React.useState([{ name: 'Yes' }, { name: 'No' }]); 73 | const { data: { loading, error, getQuestionnaire } } = props.getQuestionnaire; 74 | 75 | function handleAdd() { 76 | props.onCreateSurveyEntries({ id: group }) 77 | responses.map((response) => { 78 | props.onCreateResponse( 79 | { 80 | responsesQuId: response.responsesQuId, 81 | res: response.res, 82 | responsesGroupId: group 83 | } 84 | ); 85 | return () 86 | }) 87 | props.history.goBack() 88 | return null 89 | } 90 | 91 | function handleDateChange(id, index, event) { 92 | let newResponses = responses.slice(0); 93 | let data = { "responsesQuId": id, "res": event } 94 | newResponses[index] = data 95 | setResponses(newResponses) 96 | } 97 | 98 | function handleChange(index, event) { 99 | let newResponses = responses.slice(0); 100 | let data = { "responsesQuId": event.target.id, "res": event.target.value } 101 | newResponses[index] = data 102 | setResponses(newResponses) 103 | } 104 | 105 | function handleRadioChange(event) { 106 | setRadioValue(event.target.value); 107 | } 108 | 109 | if (loading) { 110 | return ( 111 |
112 | 113 |
114 | ); 115 | }; 116 | if (error) { 117 | console.log(error) 118 | return ( 119 |
120 | 121 | 122 | Error 123 | 124 | 125 | An error occured while fetching data. 126 | 127 | 128 | {error} 129 | 130 | 131 |
132 | ) 133 | }; 134 | return ( 135 |
136 |
137 | 138 | 139 | {getQuestionnaire.question.items.map((item, index, array) => { 140 | switch (item.type) { 141 | case 'BOOL': 142 | return ( 143 |
144 | {item.qu} 145 | 152 | {yesno.map((value, index) => { 153 | return ( 154 | } label={value.name} /> 155 | ); 156 | })} 157 | 158 |
159 | ) 160 | case 'DATETIME': 161 | return ( 162 |
163 | {item.qu} 164 | 165 | 166 | handleDateChange(item.id, index, event)} 171 | className={classes.date} 172 | /> 173 | 174 | 175 |
176 | ) 177 | case 'TEXT': 178 | return ( 179 |
180 | {item.qu} 181 | handleChange(index, event)} 190 | InputLabelProps={{ shrink: true }} 191 | /> 192 |
193 | ) 194 | case 'LIST': 195 | return ( 196 |
197 | {item.qu} 198 | handleChange(index, event)} 207 | /> 208 |
209 | ) 210 | default: 211 | return ( 212 |
213 | {item.qu} 214 | handleChange(index, event)} 223 | /> 224 |
225 | ) 226 | } 227 | })} 228 |
229 | 230 | 234 | 235 |
236 |
237 |
238 | ) 239 | }; 240 | 241 | const AddEntry = compose( 242 | graphql(gql(getQuestionnaire), { 243 | options: (props) => ({ 244 | fetchPolicy: 'cache-and-network', 245 | errorPolicy: 'all', 246 | variables: { id: props.match.params.questionnaireID }, 247 | }), 248 | props: (props) => { 249 | return { 250 | getQuestionnaire: props ? props : [], 251 | } 252 | } 253 | }), 254 | graphql(gql(createSurveyEntries), { 255 | options: (props) => ({ 256 | errorPolicy: 'all', 257 | }), 258 | props: (props) => ({ 259 | onCreateSurveyEntries: (id) => { 260 | props.mutate({ 261 | variables: { 262 | input: id 263 | }, 264 | }) 265 | } 266 | }) 267 | }), 268 | graphql(gql(createResponses), { 269 | options: (props) => ({ 270 | errorPolicy: 'all', 271 | }), 272 | props: (props) => ({ 273 | onCreateResponse: (response) => { 274 | props.mutate({ 275 | variables: { 276 | input: response 277 | }, 278 | }) 279 | } 280 | }) 281 | }) 282 | )(AddEntryPart) 283 | 284 | export default withApollo(AddEntry) -------------------------------------------------------------------------------- /src/components/admin/groups.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Typography from '@material-ui/core/Typography'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import Table from '@material-ui/core/Table'; 6 | import TableBody from '@material-ui/core/TableBody'; 7 | import TableCell from '@material-ui/core/TableCell'; 8 | import TableHead from '@material-ui/core/TableHead'; 9 | import TableRow from '@material-ui/core/TableRow'; 10 | import Paper from '@material-ui/core/Paper'; 11 | import Button from '@material-ui/core/Button' 12 | import EditIcon from '@material-ui/icons/Edit'; 13 | import DeleteIcon from '@material-ui/icons/Delete'; 14 | import CloseIcon from '@material-ui/icons/Close'; 15 | import CircularProgress from '@material-ui/core/CircularProgress'; 16 | import Snackbar from '@material-ui/core/Snackbar'; 17 | import IconButton from '@material-ui/core/IconButton'; 18 | 19 | import { graphql, compose, withApollo } from "react-apollo"; 20 | import gql from 'graphql-tag'; 21 | import { listGroups } from '../../graphql/queries'; 22 | import { deleteGroup } from '../../graphql/mutations' 23 | 24 | import AdminMenu from './index'; 25 | 26 | const useStyles = makeStyles(theme => ({ 27 | root: { 28 | display: 'flex', 29 | }, 30 | content: { 31 | flexGrow: 1, 32 | padding: theme.spacing(3), 33 | }, 34 | image: { 35 | width: 64, 36 | }, 37 | button: { 38 | margin: theme.spacing(1), 39 | }, 40 | })); 41 | 42 | const SurveyPart = (props) => { 43 | const classes = useStyles(); 44 | // eslint-disable-next-line 45 | const { data: { loading, error, listGroups } } = props.listGroups; 46 | const [openSnackBar, setOpenSnackBar] = React.useState(false); 47 | 48 | function handleSnackBarClick() { 49 | setOpenSnackBar(true); 50 | } 51 | 52 | function handleSnackBarClose(event, reason) { 53 | if (reason === 'clickaway') { 54 | return; 55 | } 56 | setOpenSnackBar(false); 57 | } 58 | 59 | function handleDeleteGroup(GroupName) { 60 | props.onDeleteGroup(GroupName, props.location.state.userPoolId); 61 | return null 62 | } 63 | 64 | if (loading) { 65 | return ( 66 |
67 | 68 |
69 | ); 70 | }; 71 | if (error) { 72 | console.log(error) 73 | return ( 74 |
75 | 76 | 77 | Error 78 | 79 | 80 | An error occured while fetching data. 81 | 82 | 83 | {error} 84 | 85 | 86 |
87 | ) 88 | }; 89 | return ( 90 |
91 | Sorry. Not currently implemented.} 103 | action={[ 104 | 111 | 112 | , 113 | ]} 114 | /> 115 | 116 |
117 | 118 | Manage Groups 119 | 120 |

121 | 122 | 123 | 124 | 125 | Name 126 | Pool ID 127 | Last Modified 128 | Manage 129 | 130 | 131 | 132 | {JSON.parse(listGroups).Groups ? JSON.parse(listGroups).Groups.map(group => ( 133 | 134 | {group.GroupName} 135 | {group.UserPoolId} 136 | {group.LastModifiedDate.toString()} 137 | 138 | 141 | 144 | 145 | 146 | )) : null} 147 | 148 |
149 |
150 |

151 |
152 | ) 153 | } 154 | 155 | const Survey = compose( 156 | graphql(gql(listGroups), { 157 | options: (props) => ({ 158 | errorPolicy: 'all', 159 | fetchPolicy: 'cache-and-network', 160 | variables: { 161 | UserPoolId: props.location.state.userPoolId, 162 | } 163 | }), 164 | props: (props) => { 165 | return { 166 | listGroups: props ? props : [], 167 | } 168 | } 169 | }), 170 | graphql(gql(deleteGroup), { 171 | options: (props) => ({ 172 | errorPolicy: 'all', 173 | }), 174 | props: (props) => ({ 175 | onDeleteGroup: (GroupName, userPoolId) => { 176 | props.mutate({ 177 | variables: { 178 | UserPoolId: userPoolId, 179 | GroupName: GroupName 180 | }, 181 | }) 182 | } 183 | }) 184 | }) 185 | )(SurveyPart) 186 | 187 | export default withApollo(Survey) -------------------------------------------------------------------------------- /src/components/admin/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { Auth } from 'aws-amplify'; 5 | 6 | import Divider from '@material-ui/core/Divider'; 7 | import Drawer from '@material-ui/core/Drawer'; 8 | import Hidden from '@material-ui/core/Hidden'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import List from '@material-ui/core/List'; 11 | import ListItem from '@material-ui/core/ListItem'; 12 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 13 | import ListItemText from '@material-ui/core/ListItemText'; 14 | import MenuIcon from '@material-ui/icons/Menu'; 15 | import QuestionAnswerIcon from '@material-ui/icons/QuestionAnswer'; 16 | import ChatBubbleOutlineIcon from '@material-ui/icons/ChatBubbleOutline'; 17 | import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle'; 18 | import PersonIcon from '@material-ui/icons/Person'; 19 | import LanguageIcon from '@material-ui/icons/Language'; 20 | import { makeStyles, useTheme } from '@material-ui/core/styles'; 21 | 22 | const drawerWidth = 240; 23 | 24 | const useStyles = makeStyles(theme => ({ 25 | root: { 26 | display: 'flex', 27 | }, 28 | drawer: { 29 | [theme.breakpoints.up('sm')]: { 30 | width: drawerWidth, 31 | flexShrink: 0, 32 | }, 33 | flexShrink: 0, 34 | }, 35 | appBar: { 36 | marginLeft: drawerWidth, 37 | [theme.breakpoints.up('sm')]: { 38 | width: `calc(100% - ${drawerWidth}px)`, 39 | }, 40 | }, 41 | menuButton: { 42 | marginRight: theme.spacing(2), 43 | [theme.breakpoints.up('sm')]: { 44 | display: 'none', 45 | }, 46 | }, 47 | toolbar: theme.mixins.toolbar, 48 | drawerPaper: { 49 | width: drawerWidth, 50 | top: 64, 51 | }, 52 | content: { 53 | flexGrow: 1, 54 | padding: theme.spacing(3), 55 | }, 56 | })); 57 | 58 | const Admin = (props) => { 59 | const { container } = props; 60 | const classes = useStyles(); 61 | const theme = useTheme(); 62 | const [mobileOpen, setMobileOpen] = React.useState(false); 63 | const [userPoolId] = React.useState(Auth.userPool.userPoolId); 64 | 65 | function handleDrawerToggle() { 66 | setMobileOpen(!mobileOpen); 67 | } 68 | 69 | const drawer = ( 70 |
71 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 96 | 97 | 98 | 99 | 105 | 106 | 107 | 108 | 109 |
110 | ); 111 | 112 | return ( 113 |
114 |
115 | 122 | 123 | 124 |
125 | 155 |
156 | ) 157 | } 158 | 159 | export default Admin -------------------------------------------------------------------------------- /src/components/admin/question.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Typography from '@material-ui/core/Typography'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import Table from '@material-ui/core/Table'; 6 | import TableBody from '@material-ui/core/TableBody'; 7 | import TableCell from '@material-ui/core/TableCell'; 8 | import TableHead from '@material-ui/core/TableHead'; 9 | import TableRow from '@material-ui/core/TableRow'; 10 | import Paper from '@material-ui/core/Paper'; 11 | import Button from '@material-ui/core/Button' 12 | import AddCircleIcon from '@material-ui/icons/AddCircle'; 13 | import EditIcon from '@material-ui/icons/Edit'; 14 | import DeleteIcon from '@material-ui/icons/Delete'; 15 | import CircularProgress from '@material-ui/core/CircularProgress'; 16 | import TextField from '@material-ui/core/TextField'; 17 | import Dialog from '@material-ui/core/Dialog'; 18 | import DialogActions from '@material-ui/core/DialogActions'; 19 | import DialogContent from '@material-ui/core/DialogContent'; 20 | import DialogContentText from '@material-ui/core/DialogContentText'; 21 | import DialogTitle from '@material-ui/core/DialogTitle'; 22 | import FormControl from '@material-ui/core/FormControl'; 23 | import MenuItem from '@material-ui/core/MenuItem'; 24 | import Select from '@material-ui/core/Select'; 25 | import InputLabel from '@material-ui/core/InputLabel'; 26 | import CloseIcon from '@material-ui/icons/Close'; 27 | import Snackbar from '@material-ui/core/Snackbar'; 28 | import IconButton from '@material-ui/core/IconButton'; 29 | 30 | import { graphql, compose, withApollo } from "react-apollo"; 31 | import gql from 'graphql-tag'; 32 | import { listQuestions, listQuestionnaires } from '../../graphql/queries'; 33 | import { createQuestion, deleteQuestion } from '../../graphql/mutations'; 34 | 35 | import AdminMenu from './index'; 36 | 37 | const useStyles = makeStyles(theme => ({ 38 | root: { 39 | display: 'flex', 40 | }, 41 | content: { 42 | flexGrow: 1, 43 | padding: theme.spacing(3), 44 | }, 45 | image: { 46 | width: 64, 47 | }, 48 | button: { 49 | margin: theme.spacing(1), 50 | }, 51 | })); 52 | 53 | const QuestionPart = (props) => { 54 | const classes = useStyles(); 55 | const { data: { loading, error, listQuestions } } = props.listQuestions; 56 | const { data: { listQuestionnaires } } = props.listQuestionnaires 57 | const [openSnackBar, setOpenSnackBar] = React.useState(false); 58 | const [open, setOpen] = React.useState(false); 59 | const [question, setQuestion] = React.useState(''); 60 | const [questionnaire, setQuestionnaire] = React.useState(''); 61 | const [type, setType] = React.useState(''); 62 | 63 | function handleSnackBarClick() { 64 | setOpenSnackBar(true); 65 | } 66 | 67 | function handleSnackBarClose(event, reason) { 68 | if (reason === 'clickaway') { 69 | return; 70 | } 71 | setOpenSnackBar(false); 72 | } 73 | 74 | function handleDelete(id) { 75 | props.onDeleteQuestion( 76 | { 77 | id: id 78 | } 79 | ); 80 | } 81 | 82 | function handleOpenDialog() { 83 | setOpen(true); 84 | } 85 | 86 | function handleCreate(event) { 87 | event.preventDefault() 88 | props.onCreateQuestion( 89 | { 90 | qu: question, 91 | type: type, 92 | questionQuestionnaireId: questionnaire 93 | }, 94 | questionnaire 95 | ) 96 | setOpen(false); 97 | } 98 | 99 | function handleClose() { 100 | setOpen(false); 101 | } 102 | 103 | function onQuestionChange(newValue) { 104 | if (question === newValue) { 105 | setQuestion(newValue); 106 | return; 107 | } 108 | setQuestion(newValue); 109 | }; 110 | 111 | function onQuestionnaireChange(newValue) { 112 | if (questionnaire === newValue) { 113 | setQuestionnaire(newValue); 114 | return; 115 | } 116 | setQuestionnaire(newValue); 117 | }; 118 | 119 | function onTypeChange(newValue) { 120 | if (type === newValue) { 121 | setType(newValue); 122 | return; 123 | } 124 | setType(newValue); 125 | }; 126 | 127 | if (loading) { 128 | return ( 129 |
130 | 131 |
132 | ); 133 | }; 134 | if (error) { 135 | console.log(error) 136 | return ( 137 |
138 | 139 | 140 | Error 141 | 142 | 143 | An error occured while fetching data. 144 | 145 | 146 | {error} 147 | 148 | 149 |
150 | ) 151 | }; 152 | return ( 153 |
154 | Sorry. Not currently implemented.} 166 | action={[ 167 | 174 | 175 | , 176 | ]} 177 | /> 178 | 179 |
180 | 181 | 182 | Create Question 183 | 184 | 185 | To create a new Question, please complete the following details. 186 | 187 | onQuestionChange(event.target.value)} 194 | fullWidth 195 | /> 196 | 197 | Type 198 | 207 |
208 | 209 | Questionnaire 210 | 219 | 220 |
221 | 222 | 225 | 228 | 229 |
230 |
231 |
232 |
233 | 234 | Manage Questions 235 | 236 |

237 | 238 | 239 | 240 | 241 | Question 242 | Type 243 | List Options 244 | Manage 245 | 246 | 247 | 248 | {listQuestions.items.map(question => ( 249 | 250 | {question.qu} 251 | {question.type} 252 | {question.listOptions ? question.listOptions.map((option) => (
  • {option}
  • )) : "(Empty)"}
    253 | 254 | 257 | 260 | 261 |
    262 | ))} 263 |
    264 |
    265 |
    266 | 269 |

    270 |
    271 | ) 272 | } 273 | 274 | const Question = compose( 275 | graphql(gql(listQuestions), { 276 | options: (props) => ({ 277 | errorPolicy: 'all', 278 | fetchPolicy: 'cache-and-network', 279 | }), 280 | props: (props) => { 281 | return { 282 | listQuestions: props ? props : [], 283 | } 284 | } 285 | }), 286 | graphql(gql(listQuestionnaires), { 287 | options: (props) => ({ 288 | errorPolicy: 'all', 289 | fetchPolicy: 'cache-and-network', 290 | }), 291 | props: (props) => { 292 | return { 293 | listQuestionnaires: props ? props : [], 294 | } 295 | } 296 | }), 297 | graphql(gql(deleteQuestion), { 298 | props: (props) => ({ 299 | onDeleteQuestion: (response) => { 300 | props.mutate({ 301 | variables: { 302 | input: response 303 | }, 304 | }) 305 | } 306 | }) 307 | }), 308 | graphql(gql(createQuestion), { 309 | props: (props) => ({ 310 | onCreateQuestion: (response) => { 311 | props.mutate({ 312 | variables: { 313 | input: response 314 | }, 315 | update: (store, { data: { createQuestion } }) => { 316 | const query = gql(listQuestions) 317 | const data = store.readQuery({query, variables: { "filter":null,"limit":null,"nextToken":null}}); 318 | data.listQuestions.items = [ 319 | ...data.listQuestions.items.filter(item => item.id !== createQuestion.id), 320 | createQuestion 321 | ]; 322 | store.writeQuery({ query, data, variables: { "filter":null,"limit":null,"nextToken":null} }); 323 | } 324 | }) 325 | }, 326 | }) 327 | }) 328 | )(QuestionPart) 329 | 330 | export default withApollo(Question) -------------------------------------------------------------------------------- /src/components/admin/questionnaire.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Typography from '@material-ui/core/Typography'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import Table from '@material-ui/core/Table'; 6 | import TableBody from '@material-ui/core/TableBody'; 7 | import TableCell from '@material-ui/core/TableCell'; 8 | import TableHead from '@material-ui/core/TableHead'; 9 | import TableRow from '@material-ui/core/TableRow'; 10 | import Paper from '@material-ui/core/Paper'; 11 | import Button from '@material-ui/core/Button' 12 | import AddCircleIcon from '@material-ui/icons/AddCircle'; 13 | import EditIcon from '@material-ui/icons/Edit'; 14 | import DeleteIcon from '@material-ui/icons/Delete'; 15 | import CircularProgress from '@material-ui/core/CircularProgress'; 16 | import TextField from '@material-ui/core/TextField'; 17 | import Dialog from '@material-ui/core/Dialog'; 18 | import DialogActions from '@material-ui/core/DialogActions'; 19 | import DialogContent from '@material-ui/core/DialogContent'; 20 | import DialogContentText from '@material-ui/core/DialogContentText'; 21 | import DialogTitle from '@material-ui/core/DialogTitle'; 22 | import FormControl from '@material-ui/core/FormControl'; 23 | import MenuItem from '@material-ui/core/MenuItem'; 24 | import Select from '@material-ui/core/Select'; 25 | import InputLabel from '@material-ui/core/InputLabel'; 26 | import CloseIcon from '@material-ui/icons/Close'; 27 | import Snackbar from '@material-ui/core/Snackbar'; 28 | import IconButton from '@material-ui/core/IconButton'; 29 | 30 | import { graphql, compose, withApollo } from "react-apollo"; 31 | import gql from 'graphql-tag'; 32 | import { listQuestionnaires, listSurveys } from '../../graphql/queries'; 33 | import { createQuestionnaire, updateSurvey, deleteQuestionnaire } from '../../graphql/mutations'; 34 | 35 | import AdminMenu from './index'; 36 | 37 | const useStyles = makeStyles(theme => ({ 38 | root: { 39 | display: 'flex', 40 | }, 41 | content: { 42 | flexGrow: 1, 43 | padding: theme.spacing(3), 44 | }, 45 | image: { 46 | width: 64, 47 | }, 48 | button: { 49 | margin: theme.spacing(1), 50 | }, 51 | })); 52 | 53 | const QuestionnairePart = (props) => { 54 | const classes = useStyles(); 55 | const { data: { loading, error, listQuestionnaires } } = props.listQuestionnaires; 56 | const { data: {listSurveys} } = props.listSurveys; 57 | const [open, setOpen] = React.useState(false); 58 | const [name, setName] = React.useState(''); 59 | const [survey, setSurvey] = React.useState(''); 60 | const [description, setDescription] = React.useState(''); 61 | const [type, setType] = React.useState(''); 62 | const [openSnackBar, setOpenSnackBar] = React.useState(false); 63 | 64 | function handleSnackBarClick() { 65 | setOpenSnackBar(true); 66 | } 67 | 68 | function handleSnackBarClose(event, reason) { 69 | if (reason === 'clickaway') { 70 | return; 71 | } 72 | setOpenSnackBar(false); 73 | } 74 | 75 | function handleOpenDialog() { 76 | setOpen(true); 77 | } 78 | 79 | function handleCreate(event) { 80 | event.preventDefault() 81 | props.onCreateQuestionnaire( 82 | { 83 | name: name, 84 | description: description, 85 | type: type 86 | }, 87 | survey 88 | ) 89 | setOpen(false); 90 | } 91 | 92 | function handleDelete(id) { 93 | props.onDeleteQuestionnaire( 94 | { 95 | id: id 96 | } 97 | ); 98 | } 99 | 100 | function handleClose() { 101 | setOpen(false); 102 | } 103 | 104 | function onNameChange(newValue) { 105 | if (name === newValue) { 106 | setName(newValue); 107 | return; 108 | } 109 | setName(newValue); 110 | }; 111 | 112 | function onDescriptionChange(newValue) { 113 | if (description === newValue) { 114 | setDescription(newValue); 115 | return; 116 | } 117 | setDescription(newValue); 118 | }; 119 | function onSurveyChange(newValue) { 120 | if (survey === newValue) { 121 | setSurvey(newValue); 122 | return; 123 | } 124 | setSurvey(newValue); 125 | }; 126 | 127 | function onTypeChange(newValue) { 128 | if (type === newValue) { 129 | setType(newValue); 130 | return; 131 | } 132 | setType(newValue); 133 | }; 134 | 135 | if (loading) { 136 | return ( 137 |
    138 | 139 |
    140 | ); 141 | }; 142 | if (error) { 143 | console.log(error) 144 | return ( 145 |
    146 | 147 | 148 | Error 149 | 150 | 151 | An error occured while fetching data. 152 | 153 | 154 | {error} 155 | 156 | 157 |
    158 | ) 159 | }; 160 | return ( 161 |
    162 | Sorry. Not currently implemented.} 174 | action={[ 175 | 182 | 183 | , 184 | ]} 185 | /> 186 | 187 |
    188 | 189 | 190 | Create Questionnaire 191 | 192 | 193 | To create a new Questionnaire, please complete the following details. 194 | 195 | onNameChange(event.target.value)} 202 | fullWidth 203 | /> 204 | onDescriptionChange(event.target.value)} 210 | fullWidth 211 | /> 212 | 213 | Survey 214 | 223 |
    224 | 225 | Type 226 | 235 | 236 |
    237 | 238 | 241 | 244 | 245 |
    246 |
    247 |
    248 |
    249 | 250 | Manage Questionnaires 251 | 252 |

    253 | 254 | 255 | 256 | 257 | Name 258 | Description 259 | Type 260 | Manage 261 | 262 | 263 | 264 | {listQuestionnaires.items.map(questionnaire => ( 265 | 266 | {questionnaire.name} 267 | {questionnaire.description} 268 | {questionnaire.type} 269 | 270 | 273 | 276 | 277 | 278 | ))} 279 | 280 |
    281 |
    282 | 285 |

    286 |
    287 | ) 288 | } 289 | 290 | const Questionnaire = compose( 291 | graphql(gql(listQuestionnaires), { 292 | options: (props) => ({ 293 | errorPolicy: 'all', 294 | fetchPolicy: 'cache-and-network', 295 | }), 296 | props: (props) => { 297 | return { 298 | listQuestionnaires: props ? props : [], 299 | } 300 | } 301 | }), 302 | graphql(gql(deleteQuestionnaire), { 303 | options: (props) => ({ 304 | errorPolicy: 'all', 305 | }), 306 | props: (props) => ({ 307 | onDeleteQuestionnaire: (questionnaire) => { 308 | props.mutate({ 309 | variables: { 310 | input: questionnaire 311 | } 312 | }) 313 | }, 314 | }), 315 | }), 316 | graphql(gql(listSurveys), { 317 | options: (props) => ({ 318 | errorPolicy: 'all', 319 | fetchPolicy: 'cache-and-network', 320 | }), 321 | props: (props) => { 322 | return { 323 | listSurveys: props ? props : [], 324 | } 325 | } 326 | }), 327 | graphql(gql(updateSurvey), { 328 | props: (props) => ({ 329 | onUpdateSurvey: (response) => { 330 | props.mutate({ 331 | variables: { 332 | input: response 333 | }, 334 | }) 335 | .then((data) => { 336 | //console.log(data) 337 | }) 338 | } 339 | }) 340 | }), 341 | graphql(gql(createQuestionnaire), { 342 | props: (props) => ({ 343 | onCreateQuestionnaire: (response,survey) => { 344 | props.mutate({ 345 | variables: { 346 | input: response 347 | }, 348 | update: (store, { data: { createQuestionnaire } }) => { 349 | const query = gql(listQuestionnaires) 350 | const data = store.readQuery({query, variables: { "filter":null,"limit":null,"nextToken":null}}); 351 | data.listQuestionnaires.items = [ 352 | ...data.listQuestionnaires.items.filter(item => item.id !== createQuestionnaire.id), 353 | createQuestionnaire 354 | ]; 355 | store.writeQuery({ query, data, variables: { "filter":null,"limit":null,"nextToken":null} }); 356 | } 357 | }) 358 | .then((data) => { 359 | var surveyData = {} 360 | switch(data.data.createQuestionnaire.type){ 361 | case "PRE": 362 | surveyData = { 363 | id: survey, 364 | surveyPreQuestionnaireId: data.data.createQuestionnaire.id 365 | } 366 | break; 367 | case "MAIN": 368 | surveyData = { 369 | id: survey, 370 | surveyMainQuestionnaireId: data.data.createQuestionnaire.id 371 | } 372 | break; 373 | case "POST": 374 | surveyData = { 375 | id: survey, 376 | surveyPostQuestionnaireId: data.data.createQuestionnaire.id 377 | } 378 | break; 379 | default: 380 | break; 381 | } 382 | props.ownProps.onUpdateSurvey(surveyData) 383 | }) 384 | } 385 | }) 386 | }) 387 | )(QuestionnairePart) 388 | 389 | export default withApollo(Questionnaire) -------------------------------------------------------------------------------- /src/components/admin/survey.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { v4 as uuid } from 'uuid'; 4 | 5 | import Typography from '@material-ui/core/Typography'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import Table from '@material-ui/core/Table'; 8 | import TableBody from '@material-ui/core/TableBody'; 9 | import TableCell from '@material-ui/core/TableCell'; 10 | import TableHead from '@material-ui/core/TableHead'; 11 | import TableRow from '@material-ui/core/TableRow'; 12 | import Paper from '@material-ui/core/Paper'; 13 | import Button from '@material-ui/core/Button' 14 | import AddCircleIcon from '@material-ui/icons/AddCircle'; 15 | import SubjectIcon from '@material-ui/icons/Subject'; 16 | import EditIcon from '@material-ui/icons/Edit'; 17 | import DeleteIcon from '@material-ui/icons/Delete'; 18 | import TextField from '@material-ui/core/TextField'; 19 | import Dialog from '@material-ui/core/Dialog'; 20 | import DialogActions from '@material-ui/core/DialogActions'; 21 | import DialogContent from '@material-ui/core/DialogContent'; 22 | import DialogContentText from '@material-ui/core/DialogContentText'; 23 | import DialogTitle from '@material-ui/core/DialogTitle'; 24 | import FormControl from '@material-ui/core/FormControl'; 25 | import CircularProgress from '@material-ui/core/CircularProgress'; 26 | 27 | import CloseIcon from '@material-ui/icons/Close'; 28 | import Snackbar from '@material-ui/core/Snackbar'; 29 | import IconButton from '@material-ui/core/IconButton'; 30 | 31 | import { graphql, compose, withApollo } from "react-apollo"; 32 | import gql from 'graphql-tag'; 33 | import { listSurveys } from '../../graphql/queries'; 34 | import { createSurvey, deleteSurvey, addGroup } from '../../graphql/mutations'; 35 | import { bulkImportSurvey } from '../../graphql/bulk'; 36 | 37 | import AdminMenu from './index'; 38 | 39 | const useStyles = makeStyles(theme => ({ 40 | root: { 41 | display: 'flex', 42 | }, 43 | content: { 44 | flexGrow: 1, 45 | padding: theme.spacing(3), 46 | }, 47 | image: { 48 | width: 64, 49 | }, 50 | button: { 51 | margin: theme.spacing(1), 52 | }, 53 | })); 54 | 55 | const SurveyPart = (props) => { 56 | const classes = useStyles(); 57 | const { data: { loading, error, listSurveys } } = props.listSurveys; 58 | const [open, setOpen] = React.useState(false); 59 | const [openSnackBar, setOpenSnackBar] = React.useState(false); 60 | const [title, setTitle] = React.useState(''); 61 | const [description, setDescription] = React.useState(''); 62 | const [groupName, setGroupName] = React.useState(''); 63 | const [image, setImage] = React.useState('https://source.unsplash.com/random'); 64 | 65 | function handleSnackBarClick() { 66 | setOpenSnackBar(true); 67 | } 68 | 69 | function handleSnackBarClose(event, reason) { 70 | if (reason === 'clickaway') { 71 | return; 72 | } 73 | 74 | setOpenSnackBar(false); 75 | } 76 | 77 | function handleOpenDialog() { 78 | setOpen(true); 79 | } 80 | 81 | function handleCreate(event) { 82 | event.preventDefault() 83 | props.onCreateSurvey( 84 | { 85 | name: title, 86 | description: description, 87 | image: image, 88 | groups: groupName 89 | } 90 | ); 91 | props.onAddGroup(groupName, props.location.state.userPoolId) 92 | setOpen(false); 93 | } 94 | 95 | function handleBulkImport(event) { 96 | event.preventDefault() 97 | props.onBulkImport() 98 | } 99 | 100 | function handleDelete(id) { 101 | props.onDeleteSurvey( 102 | { 103 | id: id 104 | } 105 | ); 106 | } 107 | 108 | function handleClose() { 109 | setOpen(false); 110 | } 111 | 112 | function onTitleChange(newValue) { 113 | if (title === newValue) { 114 | setTitle(newValue); 115 | return; 116 | } 117 | setTitle(newValue); 118 | }; 119 | 120 | function onGroupNameChange(newValue) { 121 | if (groupName === newValue) { 122 | setGroupName(newValue); 123 | return; 124 | } 125 | setGroupName(newValue); 126 | }; 127 | 128 | function onDescriptionChange(newValue) { 129 | if (title === newValue) { 130 | setDescription(newValue); 131 | return; 132 | } 133 | setDescription(newValue); 134 | }; 135 | 136 | function onImageChange(newValue) { 137 | if (title === newValue) { 138 | setImage(newValue); 139 | return; 140 | } 141 | setImage(newValue); 142 | }; 143 | 144 | if (loading) { 145 | return ( 146 |
    147 | 148 |
    149 | ); 150 | }; 151 | if (error) { 152 | console.log(error) 153 | return ( 154 |
    155 | 156 | 157 | Error 158 | 159 | 160 | An error occured while fetching data. 161 | 162 | 163 | {error} 164 | 165 | 166 |
    167 | ) 168 | }; 169 | return ( 170 |
    171 | Sorry. Not currently implemented.} 183 | action={[ 184 | 191 | 192 | , 193 | ]} 194 | /> 195 | 196 |
    197 | 198 | 199 | Create Survey 200 | 201 | 202 | To create a new Survey, please complete the following details. 203 | 204 | onTitleChange(event.target.value)} 211 | fullWidth 212 | /> 213 | onDescriptionChange(event.target.value)} 219 | fullWidth 220 | /> 221 | onImageChange(event.target.value)} 227 | fullWidth 228 | /> 229 | onGroupNameChange(event.target.value)} 235 | fullWidth 236 | /> 237 | 238 | 239 | 242 | 245 | 246 | 247 | 248 |
    249 |
    250 | 251 | Manage Surveys 252 | 253 |

    254 | 255 | 256 | 257 | 258 | 259 | Name 260 | Description 261 | Manage 262 | 263 | 264 | 265 | {listSurveys.items.map(survey => ( 266 | 267 | {survey.image} 268 | {survey.name} 269 | {survey.description} 270 | 271 | 274 | 277 | 278 | 279 | ))} 280 | 281 |
    282 |
    283 | 286 | 289 |

    290 |
    291 | ) 292 | } 293 | 294 | const Survey = compose( 295 | graphql(gql(listSurveys), { 296 | options: (props) => ({ 297 | errorPolicy: 'all', 298 | fetchPolicy: 'cache-and-network', 299 | }), 300 | props: (props) => { 301 | return { 302 | listSurveys: props ? props : [], 303 | } 304 | } 305 | }), 306 | graphql(gql(createSurvey), { 307 | options: (props) => ({ 308 | errorPolicy: 'all', 309 | }), 310 | props: (props) => ({ 311 | onCreateSurvey: (survey) => { 312 | props.mutate({ 313 | variables: { 314 | input: survey 315 | }, 316 | update: (store, { data: { createSurvey } }) => { 317 | const query = gql(listSurveys) 318 | const data = store.readQuery({query, variables: { "filter":null,"limit":null,"nextToken":null}}); 319 | data.listSurveys.items = [ 320 | ...data.listSurveys.items.filter(item => item.id !== createSurvey.id), 321 | createSurvey 322 | ]; 323 | store.writeQuery({ query, data, variables: { "filter":null,"limit":null,"nextToken":null} }); 324 | } 325 | }) 326 | }, 327 | }), 328 | }), 329 | graphql(gql(deleteSurvey), { 330 | options: (props) => ({ 331 | errorPolicy: 'all', 332 | }), 333 | props: (props) => ({ 334 | onDeleteSurvey: (survey) => { 335 | props.mutate({ 336 | variables: { 337 | input: survey 338 | } 339 | }) 340 | }, 341 | }), 342 | }), 343 | graphql(gql(addGroup), { 344 | options: (props) => ({ 345 | errorPolicy: 'all', 346 | }), 347 | props: (props) => ({ 348 | onAddGroup: (GroupName, userPoolId) => { 349 | props.mutate({ 350 | variables: { 351 | UserPoolId: userPoolId, 352 | GroupName: GroupName 353 | }, 354 | }) 355 | } 356 | }) 357 | }), 358 | graphql(gql(bulkImportSurvey), { 359 | options: (props) => ({ 360 | errorPolicy: 'all', 361 | }), 362 | props: (props) => ({ 363 | onBulkImport: () => { 364 | props.mutate({ 365 | variables: { 366 | surveyID: uuid(), 367 | surveyPreQuestionnaireId: uuid(), 368 | surveyMainQuestionnaireId: uuid() 369 | } 370 | }) 371 | }, 372 | }), 373 | }), 374 | )(SurveyPart) 375 | 376 | export default withApollo(Survey) -------------------------------------------------------------------------------- /src/components/admin/users.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Typography from '@material-ui/core/Typography'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import Table from '@material-ui/core/Table'; 6 | import TableBody from '@material-ui/core/TableBody'; 7 | import TableCell from '@material-ui/core/TableCell'; 8 | import TableHead from '@material-ui/core/TableHead'; 9 | import TableRow from '@material-ui/core/TableRow'; 10 | import Paper from '@material-ui/core/Paper'; 11 | import Button from '@material-ui/core/Button' 12 | import EditIcon from '@material-ui/icons/Edit'; 13 | import DeleteIcon from '@material-ui/icons/Delete'; 14 | import CircularProgress from '@material-ui/core/CircularProgress'; 15 | import CloseIcon from '@material-ui/icons/Close'; 16 | import Snackbar from '@material-ui/core/Snackbar'; 17 | import IconButton from '@material-ui/core/IconButton'; 18 | 19 | import { graphql, compose, withApollo } from "react-apollo"; 20 | import gql from 'graphql-tag'; 21 | import { listUsers } from '../../graphql/queries'; 22 | import { deleteUser, addUserToGroup } from '../../graphql/mutations'; 23 | 24 | import AdminMenu from './index'; 25 | 26 | const useStyles = makeStyles(theme => ({ 27 | root: { 28 | display: 'flex', 29 | }, 30 | content: { 31 | flexGrow: 1, 32 | padding: theme.spacing(3), 33 | }, 34 | image: { 35 | width: 64, 36 | }, 37 | button: { 38 | margin: theme.spacing(1), 39 | }, 40 | })); 41 | 42 | const UsersPart = (props) => { 43 | const classes = useStyles(); 44 | // eslint-disable-next-line 45 | const { data: { loading, error, listUsers } } = props.listUsers; 46 | const [openSnackBar, setOpenSnackBar] = React.useState(false); 47 | 48 | function handleDeleteUser(Username) { 49 | props.onDeleteUser(Username, props.location.state.userPoolId); 50 | return null 51 | } 52 | 53 | function handleSnackBarClick() { 54 | setOpenSnackBar(true); 55 | } 56 | 57 | function handleSnackBarClose(event, reason) { 58 | if (reason === 'clickaway') { 59 | return; 60 | } 61 | setOpenSnackBar(false); 62 | } 63 | 64 | if (loading) { 65 | return ( 66 |
    67 | 68 |
    69 | ); 70 | }; 71 | if (error) { 72 | console.log(error) 73 | return ( 74 |
    75 | 76 | 77 | Error 78 | 79 | 80 | An error occured while fetching data. 81 | 82 | 83 | {error} 84 | 85 | 86 |
    87 | ) 88 | }; 89 | return ( 90 |
    91 | Sorry. Not currently implemented.} 103 | action={[ 104 | 111 | 112 | , 113 | ]} 114 | /> 115 | 116 |
    117 | 118 | Manage Users 119 | 120 |

    121 | 122 | 123 | 124 | 125 | Name 126 | Phone 127 | Email 128 | Manage 129 | 130 | 131 | 132 | {JSON.parse(listUsers).Users ? JSON.parse(listUsers).Users.map(user => ( 133 | 134 | {user.Username} 135 | {user.Attributes[3].Value} 136 | {user.Attributes[4].Value} 137 | 138 | 141 | 144 | 145 | 146 | )) : null} 147 | 148 |
    149 |
    150 |

    151 |
    152 | ) 153 | } 154 | 155 | const Users = compose( 156 | graphql(gql(listUsers), { 157 | options: (props) => ({ 158 | errorPolicy: 'all', 159 | fetchPolicy: 'cache-and-network', 160 | variables: { 161 | UserPoolId: props.location.state.userPoolId, 162 | } 163 | }), 164 | props: (props) => { 165 | return { 166 | listUsers: props ? props : [], 167 | } 168 | } 169 | }), 170 | graphql(gql(deleteUser), { 171 | options: (props) => ({ 172 | errorPolicy: 'all', 173 | }), 174 | props: (props) => ({ 175 | onDeleteUser: (Username, userPoolId) => { 176 | props.mutate({ 177 | variables: { 178 | UserPoolId: userPoolId, 179 | Username: Username 180 | }, 181 | }) 182 | } 183 | }) 184 | }), 185 | graphql(gql(addUserToGroup), { 186 | options: (props) => ({ 187 | errorPolicy: 'all', 188 | }), 189 | props: (props) => ({ 190 | onAddUserToGroup: (Username, GroupName, userPoolId) => { 191 | props.mutate({ 192 | variables: { 193 | UserPoolId: userPoolId, 194 | Username: Username, 195 | GroupName: GroupName 196 | }, 197 | }) 198 | } 199 | }) 200 | }) 201 | )(UsersPart) 202 | 203 | export default withApollo(Users) -------------------------------------------------------------------------------- /src/components/app/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 50px; 7 | margin: 10px; 8 | pointer-events: none; 9 | } 10 | 11 | .Header { 12 | z-index: 999999999; 13 | } 14 | 15 | .wrapper { 16 | padding-left: 320px; 17 | } 18 | 19 | .SideNav { 20 | /* padding-top: 60px; */ 21 | margin-top: 65px; 22 | } 23 | 24 | .App-header { 25 | background-color: #282c34; 26 | min-height: 100vh; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | justify-content: center; 31 | font-size: calc(10px + 2vmin); 32 | color: white; 33 | } 34 | 35 | .App-link { 36 | color: #61dafb; 37 | } 38 | 39 | @keyframes App-logo-spin { 40 | from { 41 | transform: rotate(0deg); 42 | } 43 | to { 44 | transform: rotate(360deg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | 4 | import Amplify, { Auth } from 'aws-amplify'; 5 | import awsexports from '../../aws-exports'; 6 | import { withAuthenticator } from 'aws-amplify-react'; 7 | import { Route, BrowserRouter, Redirect, Link } from "react-router-dom"; 8 | 9 | import { createStyles, makeStyles, MuiThemeProvider } from '@material-ui/core/styles'; 10 | import AppBar from '@material-ui/core/AppBar'; 11 | import Toolbar from '@material-ui/core/Toolbar'; 12 | import Typography from '@material-ui/core/Typography'; 13 | import IconButton from '@material-ui/core/IconButton'; 14 | import HomeIcon from '@material-ui/icons/Home' 15 | import AccountCircle from '@material-ui/icons/AccountCircle'; 16 | import MenuItem from '@material-ui/core/MenuItem'; 17 | import Menu from '@material-ui/core/Menu'; 18 | import Dialog from '@material-ui/core/Dialog'; 19 | import DialogActions from '@material-ui/core/DialogActions'; 20 | import DialogContent from '@material-ui/core/DialogContent'; 21 | import DialogContentText from '@material-ui/core/DialogContentText'; 22 | import DialogTitle from '@material-ui/core/DialogTitle'; 23 | import Button from '@material-ui/core/Button'; 24 | 25 | import Home from '../home'; 26 | import Profile from '../profile'; 27 | import Settings from '../settings'; 28 | import Questionnaire from '../questionnaire'; 29 | import Survey from '../survey'; 30 | import AddEntry from '../addentry'; 31 | import AdminSurvey from '../admin/survey'; 32 | import AdminQuestionnaire from '../admin/questionnaire'; 33 | import AdminQuestion from '../admin/question'; 34 | import AdminUser from '../admin/users'; 35 | import AdminGroup from '../admin/groups'; 36 | 37 | import { createMuiTheme } from '@material-ui/core/styles'; 38 | import orange from '@material-ui/core/colors/orange'; 39 | import indigo from '@material-ui/core/colors/indigo'; 40 | 41 | const theme = createMuiTheme({ 42 | palette: { 43 | primary: orange, 44 | secondary: indigo 45 | }, 46 | }); 47 | 48 | Amplify.configure(awsexports); 49 | 50 | const useStyles = makeStyles((theme) => 51 | createStyles({ 52 | root: { 53 | flexGrow: 1, 54 | }, 55 | menuButton: { 56 | marginRight: theme.spacing(2), 57 | }, 58 | title: { 59 | flexGrow: 1, 60 | }, 61 | appbar: { 62 | zIndex: theme.zIndex.drawer + 1, 63 | } 64 | }), 65 | ); 66 | 67 | function SignOut() { 68 | Auth.signOut({ global: true }) 69 | .then(data => { 70 | return 71 | }) 72 | .catch(err => { 73 | console.log(err) 74 | }); 75 | } 76 | 77 | function App() { 78 | 79 | const classes = useStyles(); 80 | const [auth] = React.useState(true); 81 | const [anchorEl, setAnchorEl] = React.useState(null); 82 | const open = Boolean(anchorEl); 83 | const [openDialog, setOpenDialog] = React.useState(false); 84 | const [deferredPrompt, setDeferredPrompt] = React.useState(null); 85 | // eslint-disable-next-line 86 | const [session, setSession] = React.useState({}); 87 | 88 | function handleMenu(event) { 89 | setAnchorEl(event.currentTarget); 90 | }; 91 | 92 | function handleClose() { 93 | setAnchorEl(null); 94 | }; 95 | 96 | function handleClickAdd() { 97 | setOpenDialog(false); 98 | // Show the prompt 99 | deferredPrompt.prompt(); 100 | // Wait for the user to respond to the prompt 101 | deferredPrompt.userChoice 102 | .then((choiceResult) => { 103 | if (choiceResult.outcome === 'accepted') { 104 | console.log('User accepted the A2HS prompt'); 105 | } else { 106 | console.log('User dismissed the A2HS prompt'); 107 | } 108 | setDeferredPrompt(null);; 109 | }); 110 | } 111 | 112 | function handleCloseDialog() { 113 | setOpenDialog(false); 114 | } 115 | 116 | React.useEffect(() => { 117 | Auth.currentSession() 118 | .then(res => { 119 | setSession(res) 120 | return (res) 121 | }) 122 | .catch(err => { 123 | console.error(err); 124 | }) 125 | }, []); 126 | 127 | function AdminConsoleLink() { 128 | if(session.accessToken){ 129 | return (session.accessToken.payload['cognito:groups'].includes('SurveyAdmins') ? 130 | Admin Console : 131 | null) 132 | } else { 133 | return null; 134 | } 135 | } 136 | 137 | React.useEffect(() => { 138 | window.addEventListener("beforeinstallprompt", (e) => { 139 | e.preventDefault(); 140 | setDeferredPrompt(e); 141 | setOpenDialog(true); 142 | }); 143 | return () => { 144 | window.removeEventListener("beforeinstallprompt", (e) => { 145 | }); 146 | }; 147 | }); 148 | 149 | return ( 150 |
    151 | 157 | {"Add to Homescreen?"} 158 | 159 | 160 | This is a PWA which can be added to your homescreen. This makes this site easier to use and also lets you work offline. 161 | 162 | 163 | 164 | 167 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | Survey Tool 181 | 182 | {auth && ( 183 |
    184 | 190 | 191 | 192 | 206 | My Profile 207 | Settings 208 | 209 | Sign Out 210 | 211 |
    212 | )} 213 |
    214 |
    215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 |
    227 |
    228 |
    229 | ); 230 | } 231 | 232 | export default withAuthenticator(App); -------------------------------------------------------------------------------- /src/components/home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { createStyles, makeStyles } from '@material-ui/core/styles'; 5 | import Card from '@material-ui/core/Card'; 6 | import CardActionArea from '@material-ui/core/CardActionArea'; 7 | import CardActions from '@material-ui/core/CardActions'; 8 | import CardContent from '@material-ui/core/CardContent'; 9 | import CardMedia from '@material-ui/core/CardMedia'; 10 | import Button from '@material-ui/core/Button'; 11 | import Typography from '@material-ui/core/Typography'; 12 | import Grid from '@material-ui/core/Grid'; 13 | import Paper from '@material-ui/core/Paper'; 14 | import CircularProgress from '@material-ui/core/CircularProgress'; 15 | 16 | import { graphql, compose, withApollo } from "react-apollo"; 17 | import gql from 'graphql-tag'; 18 | import { listSurveys } from '../../graphql/queries'; 19 | 20 | const useStyles = makeStyles((theme) => 21 | createStyles({ 22 | card: { 23 | maxWidth: 345, 24 | margin: 10, 25 | }, 26 | media: { 27 | // object-fit is not supported by IE 11. 28 | objectFit: 'cover', 29 | }, 30 | table: { 31 | minWidth: 700, 32 | }, 33 | progress: { 34 | margin: 2, 35 | }, 36 | }), 37 | ); 38 | 39 | const HomePart = (props) => { 40 | const classes = useStyles(); 41 | const { data: { loading, error, listSurveys } } = props.listSurveys; 42 | 43 | if (loading) { 44 | return ( 45 |
    46 | 47 |
    48 | ); 49 | }; 50 | if (error) { 51 | console.log(error) 52 | return ( 53 |
    54 | 55 | 56 | Error 57 | 58 | 59 | An error occured while fetching data. 60 | 61 | 62 | {error} 63 | 64 | 65 |
    66 | ) 67 | }; 68 | return ( 69 | 70 | {listSurveys.items.map(({ id, name, description, image, preQuestionnaire, postQuestionnaire }) => ( 71 | 72 | 73 | 74 | 82 | 83 | 84 | {name} 85 | 86 | 87 | {description} 88 | 89 | 90 | 91 | 92 | {preQuestionnaire ? 93 | 96 | : null 97 | } 98 | 101 | {postQuestionnaire ? 102 | 105 | : null 106 | } 107 | 108 | 109 | 110 | )) 111 | } 112 | 113 | ); 114 | } 115 | 116 | const Home = compose( 117 | graphql(gql(listSurveys), { 118 | options: (props) => ({ 119 | errorPolicy: 'all', 120 | fetchPolicy: 'cache-and-network', 121 | }), 122 | props: (props) => { 123 | return { 124 | listSurveys: props ? props : [], 125 | } 126 | } 127 | }) 128 | )(HomePart) 129 | 130 | export default withApollo(Home) -------------------------------------------------------------------------------- /src/components/multistep/index.js: -------------------------------------------------------------------------------- 1 | // Originally sourced from https://github.com/vivekkhurana/react-native-multistep-wizard 2 | 3 | import React, { Component } from 'react'; 4 | 5 | class MultiStep extends Component { 6 | 7 | constructor(props) { 8 | super(props) 9 | this.next = this.next.bind(this) 10 | this.previous = this.previous.bind(this) 11 | this.saveStepState = this.saveStepState.bind(this) 12 | this.getStepState = this.getStepState.bind(this) 13 | this.finishWizard = this.finishWizard.bind(this) 14 | this.state = { 15 | curState: 0, 16 | steplist: [], 17 | childState: [] 18 | }; 19 | 20 | for (var i = 0; i < this.props.steps.length; i++) { 21 | this.state.steplist[i] = React.cloneElement(this.props.steps[i].component, { 22 | nextFn: this.next, 23 | prevFn: this.previous, 24 | saveState: this.saveStepState, 25 | getState: this.getStepState, 26 | }) 27 | } 28 | } 29 | 30 | next() { 31 | if ((this.state.curState + 1) < this.props.steps.length) { 32 | this.setState({ curState: this.state.curState + 1 }) 33 | } 34 | if ((this.state.curState + 1) === this.props.steps.length) { 35 | this.finishWizard() 36 | } 37 | } 38 | previous() { 39 | if ((this.state.curState - 1) >= 0) { 40 | this.setState({ curState: this.state.curState - 1 }) 41 | } 42 | } 43 | saveStepState(stepNum, stateData) { 44 | var chdata = this.state.childState 45 | chdata[stepNum] = stateData 46 | this.setState({ childState: chdata }) 47 | } 48 | 49 | getStepState() { 50 | return this.state.childState 51 | } 52 | 53 | finishWizard() { 54 | this.props.onFinish(this.getStepState()) 55 | } 56 | 57 | render() { 58 | return ( 59 |
    60 | {this.state.steplist[this.state.curState]} 61 |
    62 | ) 63 | } 64 | } 65 | 66 | export default MultiStep; -------------------------------------------------------------------------------- /src/components/profile/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Auth } from 'aws-amplify'; 4 | 5 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import TextField from '@material-ui/core/TextField'; 8 | 9 | const useStyles = makeStyles((theme) => 10 | createStyles({ 11 | textField: { 12 | marginLeft: 1, 13 | marginRight: 1, 14 | }, 15 | grid: { 16 | width: '100%', 17 | }, 18 | }) 19 | ) 20 | 21 | const Profile = (props) => { 22 | 23 | const classes = useStyles(); 24 | const [profile, setProfile] = React.useState({}); 25 | const [session, setSession] = React.useState({}); 26 | 27 | React.useEffect(() => { 28 | Auth.currentSession() 29 | .then(res => { 30 | setSession(res) 31 | return (res) 32 | }) 33 | .catch(err => { 34 | console.error(err); 35 | }) 36 | Auth.currentUserInfo() 37 | .then(res => { 38 | setProfile(res) 39 | return (res) 40 | }) 41 | .catch(err => { 42 | console.error(err); 43 | }) 44 | }, []); 45 | 46 | return ( 47 |
    48 |

    Profile Page

    49 | 50 | 51 | 59 | 60 | 61 | 69 | 70 | 71 | 79 | 80 | 81 | 89 | 90 | 91 |
    92 | ); 93 | } 94 | 95 | export default Profile -------------------------------------------------------------------------------- /src/components/questionBool/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 4 | import Button from '@material-ui/core/Button'; 5 | import Container from '@material-ui/core/Container'; 6 | import Box from '@material-ui/core/Box'; 7 | import Icon from '@material-ui/core/Icon'; 8 | import FormControl from '@material-ui/core/FormControl'; 9 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 10 | import FormLabel from '@material-ui/core/FormLabel'; 11 | import RadioGroup from '@material-ui/core/RadioGroup'; 12 | import Radio from '@material-ui/core/Radio'; 13 | 14 | const useStyles = makeStyles(theme => 15 | createStyles({ 16 | button: { 17 | margin: theme.spacing(1), 18 | }, 19 | input: { 20 | display: 'none', 21 | }, 22 | leftIcon: { 23 | marginRight: theme.spacing(1), 24 | }, 25 | rightIcon: { 26 | marginLeft: theme.spacing(1), 27 | }, 28 | formControl: { 29 | margin: theme.spacing(3), 30 | }, 31 | group: { 32 | margin: theme.spacing(1, 0), 33 | }, 34 | }) 35 | ); 36 | 37 | const QuestionBool = (props) => { 38 | const { qu } = props; 39 | const { final } = props; 40 | const classes = useStyles(); 41 | const [value, setValue] = React.useState(''); 42 | const [yesno] = React.useState([{ name: 'Yes' }, { name: 'No' }]); 43 | 44 | function nextPreprocess() { 45 | props.saveState(props.index, { id: props.id, value: value.name }) 46 | props.nextFn() 47 | } 48 | 49 | function previousPreprocess() { 50 | props.saveState(props.index, { id: props.id, value: value.name }) 51 | props.prevFn() 52 | } 53 | 54 | function handleChange(event) { 55 | setValue(event.target.value); 56 | } 57 | 58 | return ( 59 | 60 | 61 | Q. {qu} 62 | 69 | {yesno.map((value, index) => { 70 | return ( 71 | } label={value.name} /> 72 | ); 73 | })} 74 | 75 | 76 | 77 | 81 | 85 | 86 | 87 | ); 88 | } 89 | 90 | export default QuestionBool -------------------------------------------------------------------------------- /src/components/questionList/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 4 | import Button from '@material-ui/core/Button'; 5 | import Container from '@material-ui/core/Container'; 6 | import Box from '@material-ui/core/Box'; 7 | import ArrowBackIcon from '@material-ui/icons/ArrowBack'; 8 | import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; 9 | import FormControl from '@material-ui/core/FormControl'; 10 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 11 | import FormLabel from '@material-ui/core/FormLabel'; 12 | import RadioGroup from '@material-ui/core/RadioGroup'; 13 | import Radio from '@material-ui/core/Radio'; 14 | 15 | const useStyles = makeStyles(theme => 16 | createStyles({ 17 | button: { 18 | margin: 5, 19 | }, 20 | input: { 21 | display: 'none', 22 | }, 23 | leftIcon: { 24 | marginRight: 5, 25 | }, 26 | rightIcon: { 27 | marginLeft: 5, 28 | }, 29 | formControl: { 30 | margin: 5, 31 | }, 32 | group: { 33 | margin: 3, 34 | }, 35 | }) 36 | ) 37 | 38 | const QuestionList = (props) => { 39 | const { qu } = props; 40 | const { listOptions } = props; 41 | const { final } = props; 42 | const classes = useStyles(); 43 | const [value, setValue] = React.useState(''); 44 | 45 | function nextPreprocess() { 46 | props.saveState(props.index, { id: props.id, value: value }) 47 | props.nextFn() 48 | } 49 | 50 | function previousPreprocess() { 51 | props.saveState(props.index, { id: props.id, value: value }) 52 | props.prevFn() 53 | } 54 | 55 | function onValueChange(event, newValue) { 56 | if (value === newValue) { 57 | setValue(newValue); 58 | return; 59 | } 60 | setValue(newValue); 61 | }; 62 | 63 | return ( 64 | 65 | 66 | Q. {qu} 67 | 74 | {listOptions.map((value, index) => { 75 | return ( 76 | } label={value} /> 77 | ); 78 | })} 79 | 80 | 81 | 82 | 86 | 90 | 91 | 92 | ); 93 | } 94 | 95 | export default QuestionList; -------------------------------------------------------------------------------- /src/components/questionText/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 4 | import Button from '@material-ui/core/Button'; 5 | import Container from '@material-ui/core/Container'; 6 | import Box from '@material-ui/core/Box'; 7 | import ArrowBackIcon from '@material-ui/icons/ArrowBack'; 8 | import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; 9 | import FormControl from '@material-ui/core/FormControl'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import FormLabel from '@material-ui/core/FormLabel'; 12 | 13 | const useStyles = makeStyles(theme => 14 | createStyles({ 15 | button: { 16 | margin: theme.spacing(1), 17 | }, 18 | input: { 19 | display: 'none', 20 | }, 21 | leftIcon: { 22 | marginRight: theme.spacing(1), 23 | }, 24 | rightIcon: { 25 | marginLeft: theme.spacing(1), 26 | }, 27 | formControl: { 28 | margin: theme.spacing(3), 29 | }, 30 | group: { 31 | margin: theme.spacing(1, 0), 32 | }, 33 | textField: { 34 | marginLeft: theme.spacing(1), 35 | marginRight: theme.spacing(1), 36 | }, 37 | } 38 | ) 39 | ); 40 | 41 | const QuestionText = (props) => { 42 | const { qu } = props; 43 | const { final } = props; 44 | const classes = useStyles(); 45 | const [value, setValue] = React.useState(''); 46 | 47 | function nextPreprocess() { 48 | props.saveState(props.index, { id: props.id, value }) 49 | props.nextFn() 50 | } 51 | 52 | function previousPreprocess() { 53 | props.saveState(props.index, { id: props.id, value }) 54 | props.prevFn() 55 | } 56 | 57 | function onValueChange(newValue) { 58 | if (value === newValue) { 59 | setValue(newValue); 60 | return; 61 | } 62 | setValue(newValue); 63 | }; 64 | 65 | return ( 66 | 67 | 68 | Q. {qu} 69 | onValueChange(event.target.value)} 79 | /> 80 | 81 | 82 | 86 | 90 | 91 | 92 | ); 93 | } 94 | 95 | export default QuestionText -------------------------------------------------------------------------------- /src/components/questionnaire/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { graphql, compose, withApollo } from 'react-apollo'; 4 | import gql from 'graphql-tag'; 5 | 6 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import ArrowBackIcon from '@material-ui/icons/ArrowBack'; 9 | import CircularProgress from '@material-ui/core/CircularProgress'; 10 | import Typography from '@material-ui/core/Typography'; 11 | import Paper from '@material-ui/core/Paper'; 12 | 13 | import MultiStep from '../../components/multistep'; 14 | import QuestionText from '../questionText'; 15 | import QuestionBool from '../questionBool'; 16 | import QuestionList from '../questionList' 17 | 18 | import { getQuestionnaire } from '../../graphql/queries'; 19 | import { createResponses } from '../../graphql/mutations'; 20 | 21 | const useStyles = makeStyles((theme) => 22 | createStyles({ 23 | card: { 24 | maxWidth: 345, 25 | }, 26 | media: { 27 | // object-fit is not supported by IE 11. 28 | objectFit: 'cover', 29 | }, 30 | table: { 31 | minWidth: 700, 32 | }, 33 | progress: { 34 | margin: theme.spacing(2), 35 | }, 36 | button: { 37 | margin: theme.spacing(2), 38 | }, 39 | } 40 | ) 41 | ) 42 | 43 | const QuestionnairePart = (props) => { 44 | const classes = useStyles(); 45 | const { data: { loading, error, getQuestionnaire } } = props.getQuestionnaire; 46 | let final = false; 47 | 48 | function finish(wizardState) { 49 | wizardState.map((response) => { 50 | props.onCreateResponse( 51 | { 52 | responsesQuId: response.id, 53 | res: response.value 54 | } 55 | ); 56 | return () 57 | }) 58 | props.history.push('/') 59 | } 60 | 61 | if (loading) { 62 | return ( 63 |
    64 | 65 |
    66 | ); 67 | }; 68 | if (error) { 69 | console.log(error) 70 | return ( 71 |
    72 | 73 | 74 | Error 75 | 76 | 77 | An error occured while fetching data. 78 | 79 | 80 | {error} 81 | 82 | 83 |
    84 | ) 85 | }; 86 | return ( 87 |
    88 |
    89 | { props.history.push('/') }}> 90 | 91 | 92 |
    93 |
    94 | { 97 | if (arr.length - 1 === index) { 98 | final = true 99 | } 100 | switch (item.type) { 101 | case 'BOOL': 102 | return { 103 | name: item.id, 104 | component: 105 | } 106 | case 'TEXT': 107 | return { 108 | name: item.id, 109 | component: 110 | } 111 | case 'LIST': 112 | return { 113 | name: item.id, 114 | component: 115 | } 116 | default: 117 | return { 118 | name: item.id, 119 | component: 120 | } 121 | } 122 | }) 123 | } 124 | onFinish={finish} /> 125 |
    126 |
    127 | ) 128 | } 129 | 130 | const Questionnaire = compose( 131 | graphql(gql(getQuestionnaire), { 132 | options: (props) => ({ 133 | errorPolicy: 'all', 134 | fetchPolicy: 'cache-and-network', 135 | variables: { id: props.match.params.questionnaireID }, 136 | }), 137 | props: (props) => { 138 | return { 139 | getQuestionnaire: props ? props : [], 140 | } 141 | } 142 | }), 143 | graphql(gql(createResponses), { 144 | props: (props) => ({ 145 | onCreateResponse: (response) => { 146 | props.mutate({ 147 | variables: { 148 | input: response 149 | }, 150 | }) 151 | } 152 | }) 153 | }) 154 | )(QuestionnairePart) 155 | 156 | export default withApollo(Questionnaire) -------------------------------------------------------------------------------- /src/components/settings/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { createStyles, makeStyles } from '@material-ui/core/styles'; 4 | import FormLabel from '@material-ui/core/FormLabel'; 5 | import FormControl from '@material-ui/core/FormControl'; 6 | import FormGroup from '@material-ui/core/FormGroup'; 7 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 8 | import FormHelperText from '@material-ui/core/FormHelperText'; 9 | import Checkbox from '@material-ui/core/Checkbox'; 10 | 11 | const useStyles = makeStyles((theme) => 12 | createStyles({ 13 | root: { 14 | display: 'flex', 15 | }, 16 | formControl: { 17 | margin: theme.spacing(3), 18 | }, 19 | }), 20 | ); 21 | 22 | const Settings = (props) => { 23 | const classes = useStyles(); 24 | 25 | return ( 26 |
    27 |
    28 | 29 | Sample Settings 30 | 31 | } 33 | label="Dark Mode" 34 | /> 35 | } 37 | label="Verbose Logging" 38 | /> 39 | 42 | } 43 | label="Send Anonymous Feedback" 44 | /> 45 | 46 | (Note that controls are non-functional.) 47 | 48 |
    49 |
    50 | ); 51 | } 52 | 53 | export default Settings -------------------------------------------------------------------------------- /src/components/survey/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import { graphql, compose, withApollo } from 'react-apollo'; 5 | import gql from 'graphql-tag'; 6 | 7 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import CircularProgress from '@material-ui/core/CircularProgress'; 10 | import Fab from '@material-ui/core/Fab'; 11 | import AddIcon from '@material-ui/icons/Add'; 12 | import Typography from '@material-ui/core/Typography'; 13 | 14 | import { getSurvey } from '../../graphql/queries'; 15 | import { listSurveyEntriess } from '../../graphql/queries'; 16 | 17 | import BigCalendar from 'react-big-calendar'; 18 | import moment from 'moment'; 19 | import 'react-big-calendar/lib/css/react-big-calendar.css'; 20 | 21 | moment.locale('en-AU'); 22 | const localizer = BigCalendar.momentLocalizer(moment) 23 | 24 | const useStyles = makeStyles(theme => 25 | createStyles({ 26 | root: { 27 | flexGrow: 1, 28 | maxHeight: '600px', 29 | overflow: 'scroll', 30 | }, 31 | paper: { 32 | padding: theme.spacing(2), 33 | textAlign: 'left', 34 | color: theme.palette.text.secondary, 35 | }, 36 | progress: { 37 | margin: 20, 38 | }, 39 | header: { 40 | margin: 20, 41 | left: 20, 42 | }, 43 | fab: { 44 | bottom: 20, 45 | right: 20, 46 | margin: 0, 47 | top: 'auto', 48 | left: 'auto', 49 | position: 'fixed', 50 | }, 51 | }) 52 | ) 53 | 54 | const SurveyPart = (props) => { 55 | const classes = useStyles(); 56 | const [events, setEvents] = React.useState([]) 57 | const { listSurveyEntriess, refetch } = props; 58 | const { data: { loading, error, getSurvey } } = props.getSurvey; 59 | 60 | React.useEffect(() => { 61 | const timer = setTimeout(() => { 62 | refetch({ 'limit': 1000 }) 63 | }, 5000); 64 | return () => clearTimeout(timer); 65 | }, [refetch]); 66 | 67 | React.useEffect(() => { 68 | var eventsOut = [] 69 | 70 | if (listSurveyEntriess.items) { 71 | listSurveyEntriess.items.map((entry) => { 72 | let start = "" 73 | let end = "" 74 | let title = "" 75 | if (entry.responses.items !== []) { 76 | const responses = entry.responses.items 77 | 78 | if (typeof responses === 'undefined') return null 79 | for (let i = 0; i < responses.length; i++) { 80 | 81 | if (responses[i].qu.qu === "Activity Start Time") { 82 | start = responses[i].res 83 | } 84 | if (responses[i].qu.qu === "Activity Finish Time") { 85 | end = responses[i].res 86 | } 87 | if (responses[i].qu.qu === "What was your main activity?") { 88 | title = responses[i].res 89 | } 90 | } 91 | 92 | eventsOut.push({ 93 | title: title, 94 | start: new Date(start), 95 | end: new Date(end), 96 | allDay: false, 97 | resource: null, 98 | }) 99 | } 100 | return null 101 | }) 102 | setEvents(eventsOut) 103 | } 104 | }, [listSurveyEntriess]); 105 | 106 | if (loading) { 107 | return ( 108 |
    109 | 110 |
    111 | ); 112 | }; 113 | if (error) { 114 | console.log(error) 115 | return ( 116 |
    117 | 118 | 119 | Error 120 | 121 | 122 | An error occured while fetching data. 123 | 124 | 125 | {error} 126 | 127 | 128 |
    129 | ) 130 | }; 131 | 132 | return ( 133 |
    134 |
    135 |

    {getSurvey.name}

    136 |
    137 |
    138 | 139 | 147 | 148 | 149 | 150 | 151 |
    152 |
    153 | ) 154 | } 155 | 156 | const Survey = compose( 157 | graphql(gql(getSurvey), { 158 | options: (props) => ({ 159 | errorPolicy: 'all', 160 | fetchPolicy: 'cache-and-network', 161 | variables: { id: props.match.params.surveyID }, 162 | }), 163 | props: (props) => { 164 | return { 165 | getSurvey: props ? props : [], 166 | } 167 | } 168 | }), 169 | graphql(gql(listSurveyEntriess), { 170 | options: (props) => ({ 171 | errorPolicy: 'all', 172 | fetchPolicy: 'cache-and-network', 173 | variables: { 'limit': 1000 } 174 | }), 175 | props: (props) => { 176 | return { 177 | listSurveyEntriess: props.data.listSurveyEntriess ? props.data.listSurveyEntriess : [], 178 | refetch: props.data.refetch 179 | } 180 | } 181 | }) 182 | )(SurveyPart) 183 | 184 | export default withApollo(Survey) -------------------------------------------------------------------------------- /src/graphql/bulk.js: -------------------------------------------------------------------------------- 1 | export const bulkImportSurvey = `mutation bulkImportSurvey($surveyID: ID, $surveyPreQuestionnaireId: ID, $surveyMainQuestionnaireId: ID) { 2 | questionnaire1: createQuestionnaire(input: { 3 | id: $surveyPreQuestionnaireId 4 | name: "Simpsons Pre Questionnaire" 5 | description: "Pre questionnaire that must be completed prior to commencing survey." 6 | type: PRE 7 | }) 8 | { 9 | id 10 | name 11 | } 12 | question1: createQuestion(input: { 13 | qu: "How strongly do you agree or disagree that the Simpsons was the best TV series ever?" 14 | type: LIST 15 | listOptions: [ 16 | "Strongly Agree", 17 | "Somewhat Agree", 18 | "Neither agree nor disagree", 19 | "Somewhat Disagree", 20 | "Strongly Disagree", 21 | "Don't Know" 22 | ] 23 | order: 0 24 | questionQuestionnaireId: $surveyPreQuestionnaireId 25 | }) 26 | { 27 | id 28 | qu 29 | } 30 | question2: createQuestion(input: { 31 | qu: "How often do you wish you were able to watch a Simpsons episode right there and then?" 32 | type: LIST 33 | listOptions: [ 34 | "Always", 35 | "Often", 36 | "Sometimes", 37 | "Rarely", 38 | "Never" 39 | ] 40 | order: 1 41 | questionQuestionnaireId: $surveyPreQuestionnaireId 42 | }) 43 | { 44 | id 45 | qu 46 | } 47 | question3: createQuestion(input: { 48 | qu: "What was the name of the racehorse Bett Midler and Krusty the Cloud co-owned?" 49 | type: TEXT 50 | order: 2 51 | questionQuestionnaireId: $surveyPreQuestionnaireId 52 | }) 53 | { 54 | id 55 | qu 56 | } 57 | questionnaire2: createQuestionnaire(input: { 58 | id: $surveyMainQuestionnaireId 59 | name: "Simpsons Main Survey" 60 | description: "Main Survey questions." 61 | type: MAIN 62 | }) 63 | { 64 | id 65 | name 66 | } 67 | question4: createQuestion(input: { 68 | qu: "Activity Start Time" 69 | type: DATETIME 70 | order: 1 71 | questionQuestionnaireId: $surveyMainQuestionnaireId 72 | }) 73 | { 74 | id 75 | qu 76 | } 77 | question5: createQuestion(input: { 78 | qu: "Activity Finish Time" 79 | type: DATETIME 80 | order: 2 81 | questionQuestionnaireId: $surveyMainQuestionnaireId 82 | }) 83 | { 84 | id 85 | qu 86 | } 87 | question6: createQuestion(input: { 88 | qu: "Where were you?" 89 | type: TEXT 90 | order: 3 91 | questionQuestionnaireId: $surveyMainQuestionnaireId 92 | }) 93 | { 94 | id 95 | qu 96 | } 97 | question7: createQuestion(input: { 98 | qu: "What episode did you watch?" 99 | type: TEXT 100 | order: 4 101 | questionQuestionnaireId: $surveyMainQuestionnaireId 102 | }) 103 | { 104 | id 105 | qu 106 | } 107 | createSurvey(input: { 108 | id: $surveyID 109 | name: "The Simpsons Survey" 110 | description: "This survey tests you on your knowledge of The Simpsons, and you are requested to add entries for each time you watched an episode of the Simpsons during the survey period." 111 | image: "https://m.media-amazon.com/images/M/MV5BYjc2MzcwMjctNjI2NC00MGQ1LWEwYmEtYWUyN2M2NjZjN2Q4XkEyXkFqcGdeQXVyNDQ2OTk4MzI@._V1_.jpg" 112 | archived: false 113 | groups: ["Simpsons"] 114 | surveyPreQuestionnaireId: $surveyPreQuestionnaireId 115 | surveyMainQuestionnaireId: $surveyMainQuestionnaireId 116 | }) 117 | { 118 | id 119 | name 120 | } 121 | } 122 | `; -------------------------------------------------------------------------------- /src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | // eslint-disable 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const deleteUser = `mutation DeleteUser($UserPoolId: String, $Username: String) { 5 | deleteUser(UserPoolId: $UserPoolId, Username: $Username) 6 | } 7 | `; 8 | export const addUserToGroup = `mutation AddUserToGroup( 9 | $UserPoolId: String 10 | $Username: String 11 | $GroupName: String 12 | ) { 13 | addUserToGroup( 14 | UserPoolId: $UserPoolId 15 | Username: $Username 16 | GroupName: $GroupName 17 | ) 18 | } 19 | `; 20 | export const addGroup = `mutation AddGroup($UserPoolId: String, $GroupName: String) { 21 | addGroup(UserPoolId: $UserPoolId, GroupName: $GroupName) 22 | } 23 | `; 24 | export const deleteGroup = `mutation DeleteGroup($UserPoolId: String, $GroupName: String) { 25 | deleteGroup(UserPoolId: $UserPoolId, GroupName: $GroupName) 26 | } 27 | `; 28 | export const createSurvey = `mutation CreateSurvey($input: CreateSurveyInput!) { 29 | createSurvey(input: $input) { 30 | id 31 | name 32 | description 33 | image 34 | preQuestionnaire { 35 | id 36 | name 37 | description 38 | type 39 | question { 40 | nextToken 41 | } 42 | } 43 | mainQuestionnaire { 44 | id 45 | name 46 | description 47 | type 48 | question { 49 | nextToken 50 | } 51 | } 52 | postQuestionnaire { 53 | id 54 | name 55 | description 56 | type 57 | question { 58 | nextToken 59 | } 60 | } 61 | archived 62 | groups 63 | } 64 | } 65 | `; 66 | export const updateSurvey = `mutation UpdateSurvey($input: UpdateSurveyInput!) { 67 | updateSurvey(input: $input) { 68 | id 69 | name 70 | description 71 | image 72 | preQuestionnaire { 73 | id 74 | name 75 | description 76 | type 77 | question { 78 | nextToken 79 | } 80 | } 81 | mainQuestionnaire { 82 | id 83 | name 84 | description 85 | type 86 | question { 87 | nextToken 88 | } 89 | } 90 | postQuestionnaire { 91 | id 92 | name 93 | description 94 | type 95 | question { 96 | nextToken 97 | } 98 | } 99 | archived 100 | groups 101 | } 102 | } 103 | `; 104 | export const deleteSurvey = `mutation DeleteSurvey($input: DeleteSurveyInput!) { 105 | deleteSurvey(input: $input) { 106 | id 107 | name 108 | description 109 | image 110 | preQuestionnaire { 111 | id 112 | name 113 | description 114 | type 115 | question { 116 | nextToken 117 | } 118 | } 119 | mainQuestionnaire { 120 | id 121 | name 122 | description 123 | type 124 | question { 125 | nextToken 126 | } 127 | } 128 | postQuestionnaire { 129 | id 130 | name 131 | description 132 | type 133 | question { 134 | nextToken 135 | } 136 | } 137 | archived 138 | groups 139 | } 140 | } 141 | `; 142 | export const createQuestionnaire = `mutation CreateQuestionnaire($input: CreateQuestionnaireInput!) { 143 | createQuestionnaire(input: $input) { 144 | id 145 | name 146 | description 147 | type 148 | question { 149 | items { 150 | id 151 | qu 152 | type 153 | listOptions 154 | order 155 | } 156 | nextToken 157 | } 158 | } 159 | } 160 | `; 161 | export const updateQuestionnaire = `mutation UpdateQuestionnaire($input: UpdateQuestionnaireInput!) { 162 | updateQuestionnaire(input: $input) { 163 | id 164 | name 165 | description 166 | type 167 | question { 168 | items { 169 | id 170 | qu 171 | type 172 | listOptions 173 | order 174 | } 175 | nextToken 176 | } 177 | } 178 | } 179 | `; 180 | export const deleteQuestionnaire = `mutation DeleteQuestionnaire($input: DeleteQuestionnaireInput!) { 181 | deleteQuestionnaire(input: $input) { 182 | id 183 | name 184 | description 185 | type 186 | question { 187 | items { 188 | id 189 | qu 190 | type 191 | listOptions 192 | order 193 | } 194 | nextToken 195 | } 196 | } 197 | } 198 | `; 199 | export const createQuestion = `mutation CreateQuestion($input: CreateQuestionInput!) { 200 | createQuestion(input: $input) { 201 | id 202 | qu 203 | type 204 | listOptions 205 | questionnaire { 206 | id 207 | name 208 | description 209 | type 210 | question { 211 | nextToken 212 | } 213 | } 214 | order 215 | } 216 | } 217 | `; 218 | export const updateQuestion = `mutation UpdateQuestion($input: UpdateQuestionInput!) { 219 | updateQuestion(input: $input) { 220 | id 221 | qu 222 | type 223 | listOptions 224 | questionnaire { 225 | id 226 | name 227 | description 228 | type 229 | question { 230 | nextToken 231 | } 232 | } 233 | order 234 | } 235 | } 236 | `; 237 | export const deleteQuestion = `mutation DeleteQuestion($input: DeleteQuestionInput!) { 238 | deleteQuestion(input: $input) { 239 | id 240 | qu 241 | type 242 | listOptions 243 | questionnaire { 244 | id 245 | name 246 | description 247 | type 248 | question { 249 | nextToken 250 | } 251 | } 252 | order 253 | } 254 | } 255 | `; 256 | export const createResponses = `mutation CreateResponses($input: CreateResponsesInput!) { 257 | createResponses(input: $input) { 258 | id 259 | qu { 260 | id 261 | qu 262 | type 263 | listOptions 264 | questionnaire { 265 | id 266 | name 267 | description 268 | type 269 | } 270 | order 271 | } 272 | res 273 | group { 274 | id 275 | responses { 276 | nextToken 277 | } 278 | } 279 | } 280 | } 281 | `; 282 | export const updateResponses = `mutation UpdateResponses($input: UpdateResponsesInput!) { 283 | updateResponses(input: $input) { 284 | id 285 | qu { 286 | id 287 | qu 288 | type 289 | listOptions 290 | questionnaire { 291 | id 292 | name 293 | description 294 | type 295 | } 296 | order 297 | } 298 | res 299 | group { 300 | id 301 | responses { 302 | nextToken 303 | } 304 | } 305 | } 306 | } 307 | `; 308 | export const deleteResponses = `mutation DeleteResponses($input: DeleteResponsesInput!) { 309 | deleteResponses(input: $input) { 310 | id 311 | qu { 312 | id 313 | qu 314 | type 315 | listOptions 316 | questionnaire { 317 | id 318 | name 319 | description 320 | type 321 | } 322 | order 323 | } 324 | res 325 | group { 326 | id 327 | responses { 328 | nextToken 329 | } 330 | } 331 | } 332 | } 333 | `; 334 | export const createSurveyEntries = `mutation CreateSurveyEntries($input: CreateSurveyEntriesInput!) { 335 | createSurveyEntries(input: $input) { 336 | id 337 | responses { 338 | items { 339 | id 340 | res 341 | } 342 | nextToken 343 | } 344 | } 345 | } 346 | `; 347 | export const updateSurveyEntries = `mutation UpdateSurveyEntries($input: UpdateSurveyEntriesInput!) { 348 | updateSurveyEntries(input: $input) { 349 | id 350 | responses { 351 | items { 352 | id 353 | res 354 | } 355 | nextToken 356 | } 357 | } 358 | } 359 | `; 360 | export const deleteSurveyEntries = `mutation DeleteSurveyEntries($input: DeleteSurveyEntriesInput!) { 361 | deleteSurveyEntries(input: $input) { 362 | id 363 | responses { 364 | items { 365 | id 366 | res 367 | } 368 | nextToken 369 | } 370 | } 371 | } 372 | `; 373 | -------------------------------------------------------------------------------- /src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | // eslint-disable 2 | // this is an auto generated file. This will be overwritten 3 | 4 | export const listUsers = `query ListUsers($UserPoolId: String) { 5 | listUsers(UserPoolId: $UserPoolId) 6 | } 7 | `; 8 | export const listGroups = `query ListGroups($UserPoolId: String) { 9 | listGroups(UserPoolId: $UserPoolId) 10 | } 11 | `; 12 | export const listGroupMembers = `query ListGroupMembers($UserPoolId: String, $GroupName: String) { 13 | listGroupMembers(UserPoolId: $UserPoolId, GroupName: $GroupName) 14 | } 15 | `; 16 | export const getSurvey = `query GetSurvey($id: ID!) { 17 | getSurvey(id: $id) { 18 | id 19 | name 20 | description 21 | image 22 | preQuestionnaire { 23 | id 24 | name 25 | description 26 | type 27 | question { 28 | nextToken 29 | } 30 | } 31 | mainQuestionnaire { 32 | id 33 | name 34 | description 35 | type 36 | question { 37 | nextToken 38 | } 39 | } 40 | postQuestionnaire { 41 | id 42 | name 43 | description 44 | type 45 | question { 46 | nextToken 47 | } 48 | } 49 | archived 50 | groups 51 | } 52 | } 53 | `; 54 | export const listSurveys = `query ListSurveys( 55 | $filter: ModelSurveyFilterInput 56 | $limit: Int 57 | $nextToken: String 58 | ) { 59 | listSurveys(filter: $filter, limit: $limit, nextToken: $nextToken) { 60 | items { 61 | id 62 | name 63 | description 64 | image 65 | preQuestionnaire { 66 | id 67 | name 68 | description 69 | type 70 | } 71 | mainQuestionnaire { 72 | id 73 | name 74 | description 75 | type 76 | } 77 | postQuestionnaire { 78 | id 79 | name 80 | description 81 | type 82 | } 83 | archived 84 | groups 85 | } 86 | nextToken 87 | } 88 | } 89 | `; 90 | export const getQuestionnaire = `query GetQuestionnaire($id: ID!) { 91 | getQuestionnaire(id: $id) { 92 | id 93 | name 94 | description 95 | type 96 | question { 97 | items { 98 | id 99 | qu 100 | type 101 | listOptions 102 | order 103 | } 104 | nextToken 105 | } 106 | } 107 | } 108 | `; 109 | export const listQuestionnaires = `query ListQuestionnaires( 110 | $filter: ModelQuestionnaireFilterInput 111 | $limit: Int 112 | $nextToken: String 113 | ) { 114 | listQuestionnaires(filter: $filter, limit: $limit, nextToken: $nextToken) { 115 | items { 116 | id 117 | name 118 | description 119 | type 120 | question { 121 | nextToken 122 | } 123 | } 124 | nextToken 125 | } 126 | } 127 | `; 128 | export const getQuestion = `query GetQuestion($id: ID!) { 129 | getQuestion(id: $id) { 130 | id 131 | qu 132 | type 133 | listOptions 134 | questionnaire { 135 | id 136 | name 137 | description 138 | type 139 | question { 140 | nextToken 141 | } 142 | } 143 | order 144 | } 145 | } 146 | `; 147 | export const listQuestions = `query ListQuestions( 148 | $filter: ModelQuestionFilterInput 149 | $limit: Int 150 | $nextToken: String 151 | ) { 152 | listQuestions(filter: $filter, limit: $limit, nextToken: $nextToken) { 153 | items { 154 | id 155 | qu 156 | type 157 | listOptions 158 | questionnaire { 159 | id 160 | name 161 | description 162 | type 163 | } 164 | order 165 | } 166 | nextToken 167 | } 168 | } 169 | `; 170 | export const getResponses = `query GetResponses($id: ID!) { 171 | getResponses(id: $id) { 172 | id 173 | qu { 174 | id 175 | qu 176 | type 177 | listOptions 178 | questionnaire { 179 | id 180 | name 181 | description 182 | type 183 | } 184 | order 185 | } 186 | res 187 | group { 188 | id 189 | responses { 190 | nextToken 191 | } 192 | } 193 | } 194 | } 195 | `; 196 | export const listResponsess = `query ListResponsess( 197 | $filter: ModelResponsesFilterInput 198 | $limit: Int 199 | $nextToken: String 200 | ) { 201 | listResponsess(filter: $filter, limit: $limit, nextToken: $nextToken) { 202 | items { 203 | id 204 | qu { 205 | id 206 | qu 207 | type 208 | listOptions 209 | order 210 | } 211 | res 212 | group { 213 | id 214 | } 215 | } 216 | nextToken 217 | } 218 | } 219 | `; 220 | export const getSurveyEntries = `query GetSurveyEntries($id: ID!) { 221 | getSurveyEntries(id: $id) { 222 | id 223 | responses { 224 | items { 225 | id 226 | res 227 | } 228 | nextToken 229 | } 230 | } 231 | } 232 | `; 233 | export const listSurveyEntriess = `query ListSurveyEntriess( 234 | $filter: ModelSurveyEntriesFilterInput 235 | $limit: Int 236 | $nextToken: String 237 | ) { 238 | listSurveyEntriess(filter: $filter, limit: $limit, nextToken: $nextToken) { 239 | items { 240 | id 241 | responses { 242 | items{ 243 | id 244 | qu{ 245 | id 246 | qu 247 | } 248 | res 249 | } 250 | nextToken 251 | } 252 | } 253 | nextToken 254 | } 255 | } 256 | `; 257 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './components/app'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | import { Auth, Analytics } from 'aws-amplify'; 8 | import AWSAppSyncClient, { AUTH_TYPE, createAppSyncLink, createLinkWithCache } from 'aws-appsync'; 9 | import awsexports from './aws-exports'; 10 | import { ApolloProvider } from 'react-apollo'; 11 | 12 | import { ApolloLink } from 'apollo-link'; 13 | import { withClientState } from 'apollo-link-state'; 14 | 15 | const stateLink = createLinkWithCache(cache => withClientState({ cache, resolvers: {}, })); 16 | const awsAppSyncLink = createAppSyncLink({ 17 | url: awsexports.aws_appsync_graphqlEndpoint, 18 | region: awsexports.aws_appsync_region, 19 | auth: { 20 | type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS, 21 | jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken() 22 | }, 23 | complexObjectsCredentials: () => Auth.currentCredentials() 24 | }); 25 | const link = ApolloLink.from([stateLink, awsAppSyncLink]); 26 | const client = new AWSAppSyncClient({}, { link }); 27 | 28 | Analytics.autoTrack('session', { 29 | enable: true, 30 | provider: 'AWSPinpoint' 31 | }); 32 | 33 | Analytics.autoTrack('pageView', { 34 | enable: true, 35 | eventName: 'pageView', 36 | type: 'SPA', 37 | provider: 'AWSPinpoint', 38 | getUrl: () => { 39 | return window.location.origin + window.location.pathname; 40 | } 41 | }); 42 | 43 | Analytics.autoTrack('event', { 44 | enable: true, 45 | events: ['click'], 46 | selectorPrefix: 'data-amplify-analytics-', 47 | provider: 'AWSPinpoint' 48 | }); 49 | 50 | ReactDOM.render( 51 | 52 | 53 | , 54 | document.getElementById('root') 55 | ); 56 | 57 | serviceWorker.register(); -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | //if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | if ('serviceWorker' in navigator) { 26 | // The URL constructor is available in all browsers that support SW. 27 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 28 | if (publicUrl.origin !== window.location.origin) { 29 | // Our service worker won't work if PUBLIC_URL is on a different origin 30 | // from what our page is served on. This might happen if a CDN is used to 31 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 32 | console.log("error in registration") 33 | return; 34 | } 35 | 36 | window.addEventListener('fetch', function(event) { 37 | event.respondWith( 38 | caches.match(event.request).then(function(response) { 39 | return response || fetch(event.request); 40 | }) 41 | ); 42 | }); 43 | 44 | window.addEventListener('load', () => { 45 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 46 | 47 | if (isLocalhost) { 48 | // This is running on localhost. Let's check if a service worker still exists or not. 49 | checkValidServiceWorker(swUrl, config); 50 | 51 | // Add some additional logging to localhost, pointing developers to the 52 | // service worker/PWA documentation. 53 | navigator.serviceWorker.ready.then(() => { 54 | console.log( 55 | 'This web app is being served cache-first by a service ' + 56 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 57 | ); 58 | }); 59 | } else { 60 | // Is not localhost. Just register service worker 61 | registerValidSW(swUrl, config); 62 | } 63 | }); 64 | } 65 | } 66 | 67 | function registerValidSW(swUrl, config) { 68 | navigator.serviceWorker 69 | .register(swUrl) 70 | .then(registration => { 71 | registration.onupdatefound = () => { 72 | const installingWorker = registration.installing; 73 | if (installingWorker == null) { 74 | return; 75 | } 76 | installingWorker.onstatechange = () => { 77 | if (installingWorker.state === 'installed') { 78 | if (navigator.serviceWorker.controller) { 79 | // At this point, the updated precached content has been fetched, 80 | // but the previous service worker will still serve the older 81 | // content until all client tabs are closed. 82 | console.log( 83 | 'New content is available and will be used when all ' + 84 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 85 | ); 86 | 87 | // Execute callback 88 | if (config && config.onUpdate) { 89 | config.onUpdate(registration); 90 | } 91 | } else { 92 | // At this point, everything has been precached. 93 | // It's the perfect time to display a 94 | // "Content is cached for offline use." message. 95 | console.log('Content is cached for offline use.'); 96 | 97 | // Execute callback 98 | if (config && config.onSuccess) { 99 | config.onSuccess(registration); 100 | } 101 | } 102 | } 103 | }; 104 | }; 105 | }) 106 | .catch(error => { 107 | console.error('Error during service worker registration:', error); 108 | }); 109 | } 110 | 111 | function checkValidServiceWorker(swUrl, config) { 112 | // Check if the service worker can be found. If it can't reload the page. 113 | fetch(swUrl) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready.then(registration => { 142 | registration.unregister(); 143 | }); 144 | } 145 | } 146 | --------------------------------------------------------------------------------