├── .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 | [](https://console.aws.amazon.com/amplify/home#/deploy?repo=https://github.com/aws-samples/aws-appsync-survey-tool)
6 |
7 | 
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 | 
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 |
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 |
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 |
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 |
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 | :
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 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | Survey Tool
181 |
182 | {auth && (
183 |
184 |
190 |
191 |
192 |
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 |
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 |
--------------------------------------------------------------------------------