├── .github
└── workflows
│ └── build-node.js.yml
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── mockServiceWorker.js
└── robots.txt
├── src
├── App.css
├── App.test.tsx
├── App.tsx
├── components
│ ├── Editor.tsx
│ ├── Surveys.css
│ ├── Surveys.tsx
│ └── Viewer.tsx
├── index.css
├── index.tsx
├── logo.svg
├── mocks
│ ├── browser.ts
│ ├── handlers.ts
│ └── server.ts
├── models
│ ├── in-memory-storage.ts
│ └── survey.ts
├── pages
│ ├── About.tsx
│ ├── Edit.tsx
│ ├── Home.tsx
│ ├── Results.tsx
│ └── Run.tsx
├── react-app-env.d.ts
├── redux
│ ├── index.ts
│ ├── results.ts
│ ├── root-reducer.ts
│ └── surveys.ts
├── reportWebVitals.ts
├── routes
│ └── index.tsx
└── setupTests.ts
└── tsconfig.json
/.github/workflows/build-node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [18.x, 20.x, 22.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v3
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'npm'
29 | - run: npm ci
30 | - run: npm run build --if-present
31 | - run: npm test
32 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/surveyjs/surveyjs-react-client/actions/workflows/build-node.js.yml)
2 |
3 | # SurveyJS React Application
4 |
5 | This project is a client-side React application that uses [SurveyJS](https://surveyjs.io/) components. The application displays a list of surveys with the following buttons that perform actions on the surveys:
6 |
7 | - **Run** - Uses the [SurveyJS Form Library](https://surveyjs.io/form-library/documentation/overview) component to run the survey.
8 | - **Edit** - Uses the [Survey Creator](https://surveyjs.io/survey-creator/documentation/overview) component to configure the survey.
9 | - **Results** - Uses the [SurveyJS Dashboard](https://surveyjs.io/dashboard/documentation/overview) component to display survey results as a table.
10 | - **Remove** - Deletes the survey.
11 |
12 | 
13 |
14 | You can integrate this project with a backend of your choice to create a full-cycle survey management service as shown in the following repos:
15 |
16 | - [surveyjs-aspnet-mvc](https://github.com/surveyjs/surveyjs-aspnet-mvc)
17 | - [surveyjs-nodejs](https://github.com/surveyjs/surveyjs-nodejs)
18 | - [surveyjs-php](https://github.com/surveyjs/surveyjs-php)
19 |
20 | ## Run the Application
21 |
22 | ```bash
23 | git clone https://github.com/surveyjs/surveyjs-react-client.git
24 | cd surveyjs-react-client
25 | npm i
26 | npm start
27 | ```
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "surveyjs-react-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.8.1",
7 | "@testing-library/jest-dom": "^5.16.4",
8 | "@testing-library/react": "^13.0.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "@types/jest": "^27.4.1",
11 | "@types/node": "^16.11.26",
12 | "@types/react": "^18",
13 | "@types/react-dom": "^18",
14 | "@types/react-redux": "^7.1.23",
15 | "@types/react-router": "^5.1.18",
16 | "@types/react-router-dom": "^5.3.3",
17 | "axios": "^0.26.1",
18 | "connected-react-router": "6.9.3",
19 | "history": "5.3.0",
20 | "react": "^18.2.0",
21 | "react-dom": "^18.2.0",
22 | "react-redux": "^7.2.8",
23 | "react-router": "6.3.0",
24 | "react-router-dom": "6.3.0",
25 | "react-scripts": "5.0.0",
26 | "survey-analytics": "latest",
27 | "survey-core": "latest",
28 | "survey-creator-core": "latest",
29 | "survey-creator-react": "latest",
30 | "survey-react-ui": "latest",
31 | "typescript": "^4.6.3",
32 | "web-vitals": "^2.1.4"
33 | },
34 | "scripts": {
35 | "start": "react-scripts start",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test",
38 | "eject": "react-scripts eject"
39 | },
40 | "eslintConfig": {
41 | "extends": [
42 | "react-app",
43 | "react-app/jest"
44 | ]
45 | },
46 | "browserslist": {
47 | "production": [
48 | ">0.2%",
49 | "not dead",
50 | "not op_mini all"
51 | ],
52 | "development": [
53 | "last 1 chrome version",
54 | "last 1 firefox version",
55 | "last 1 safari version"
56 | ]
57 | },
58 | "msw": {
59 | "workerDirectory": "public"
60 | },
61 | "devDependencies": {
62 | "msw": "^0.39.2"
63 | },
64 | "overrides": {
65 | "connected-react-router": {
66 | "react": "^18.0.0",
67 | "react-router": "6.3.0",
68 | "history": "5.3.0"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/surveyjs/surveyjs-react-client/27c011ba37bd23664dc69ae1dc1d4b3c9de5a2eb/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
25 | SurveyJS Service
26 |
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/surveyjs/surveyjs-react-client/27c011ba37bd23664dc69ae1dc1d4b3c9de5a2eb/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/surveyjs/surveyjs-react-client/27c011ba37bd23664dc69ae1dc1d4b3c9de5a2eb/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "SurveyJS Service",
3 | "name": "SurveyJS React Service Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/mockServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* tslint:disable */
3 |
4 | /**
5 | * Mock Service Worker (0.39.2).
6 | * @see https://github.com/mswjs/msw
7 | * - Please do NOT modify this file.
8 | * - Please do NOT serve this file on production.
9 | */
10 |
11 | const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929'
12 | const bypassHeaderName = 'x-msw-bypass'
13 | const activeClientIds = new Set()
14 |
15 | self.addEventListener('install', function () {
16 | return self.skipWaiting()
17 | })
18 |
19 | self.addEventListener('activate', async function (event) {
20 | return self.clients.claim()
21 | })
22 |
23 | self.addEventListener('message', async function (event) {
24 | const clientId = event.source.id
25 |
26 | if (!clientId || !self.clients) {
27 | return
28 | }
29 |
30 | const client = await self.clients.get(clientId)
31 |
32 | if (!client) {
33 | return
34 | }
35 |
36 | const allClients = await self.clients.matchAll()
37 |
38 | switch (event.data) {
39 | case 'KEEPALIVE_REQUEST': {
40 | sendToClient(client, {
41 | type: 'KEEPALIVE_RESPONSE',
42 | })
43 | break
44 | }
45 |
46 | case 'INTEGRITY_CHECK_REQUEST': {
47 | sendToClient(client, {
48 | type: 'INTEGRITY_CHECK_RESPONSE',
49 | payload: INTEGRITY_CHECKSUM,
50 | })
51 | break
52 | }
53 |
54 | case 'MOCK_ACTIVATE': {
55 | activeClientIds.add(clientId)
56 |
57 | sendToClient(client, {
58 | type: 'MOCKING_ENABLED',
59 | payload: true,
60 | })
61 | break
62 | }
63 |
64 | case 'MOCK_DEACTIVATE': {
65 | activeClientIds.delete(clientId)
66 | break
67 | }
68 |
69 | case 'CLIENT_CLOSED': {
70 | activeClientIds.delete(clientId)
71 |
72 | const remainingClients = allClients.filter((client) => {
73 | return client.id !== clientId
74 | })
75 |
76 | // Unregister itself when there are no more clients
77 | if (remainingClients.length === 0) {
78 | self.registration.unregister()
79 | }
80 |
81 | break
82 | }
83 | }
84 | })
85 |
86 | // Resolve the "main" client for the given event.
87 | // Client that issues a request doesn't necessarily equal the client
88 | // that registered the worker. It's with the latter the worker should
89 | // communicate with during the response resolving phase.
90 | async function resolveMainClient(event) {
91 | const client = await self.clients.get(event.clientId)
92 |
93 | if (client.frameType === 'top-level') {
94 | return client
95 | }
96 |
97 | const allClients = await self.clients.matchAll()
98 |
99 | return allClients
100 | .filter((client) => {
101 | // Get only those clients that are currently visible.
102 | return client.visibilityState === 'visible'
103 | })
104 | .find((client) => {
105 | // Find the client ID that's recorded in the
106 | // set of clients that have registered the worker.
107 | return activeClientIds.has(client.id)
108 | })
109 | }
110 |
111 | async function handleRequest(event, requestId) {
112 | const client = await resolveMainClient(event)
113 | const response = await getResponse(event, client, requestId)
114 |
115 | // Send back the response clone for the "response:*" life-cycle events.
116 | // Ensure MSW is active and ready to handle the message, otherwise
117 | // this message will pend indefinitely.
118 | if (client && activeClientIds.has(client.id)) {
119 | ;(async function () {
120 | const clonedResponse = response.clone()
121 | sendToClient(client, {
122 | type: 'RESPONSE',
123 | payload: {
124 | requestId,
125 | type: clonedResponse.type,
126 | ok: clonedResponse.ok,
127 | status: clonedResponse.status,
128 | statusText: clonedResponse.statusText,
129 | body:
130 | clonedResponse.body === null ? null : await clonedResponse.text(),
131 | headers: serializeHeaders(clonedResponse.headers),
132 | redirected: clonedResponse.redirected,
133 | },
134 | })
135 | })()
136 | }
137 |
138 | return response
139 | }
140 |
141 | async function getResponse(event, client, requestId) {
142 | const { request } = event
143 | const requestClone = request.clone()
144 | const getOriginalResponse = () => fetch(requestClone)
145 |
146 | // Bypass mocking when the request client is not active.
147 | if (!client) {
148 | return getOriginalResponse()
149 | }
150 |
151 | // Bypass initial page load requests (i.e. static assets).
152 | // The absence of the immediate/parent client in the map of the active clients
153 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
154 | // and is not ready to handle requests.
155 | if (!activeClientIds.has(client.id)) {
156 | return await getOriginalResponse()
157 | }
158 |
159 | // Bypass requests with the explicit bypass header
160 | if (requestClone.headers.get(bypassHeaderName) === 'true') {
161 | const cleanRequestHeaders = serializeHeaders(requestClone.headers)
162 |
163 | // Remove the bypass header to comply with the CORS preflight check.
164 | delete cleanRequestHeaders[bypassHeaderName]
165 |
166 | const originalRequest = new Request(requestClone, {
167 | headers: new Headers(cleanRequestHeaders),
168 | })
169 |
170 | return fetch(originalRequest)
171 | }
172 |
173 | // Send the request to the client-side MSW.
174 | const reqHeaders = serializeHeaders(request.headers)
175 | const body = await request.text()
176 |
177 | const clientMessage = await sendToClient(client, {
178 | type: 'REQUEST',
179 | payload: {
180 | id: requestId,
181 | url: request.url,
182 | method: request.method,
183 | headers: reqHeaders,
184 | cache: request.cache,
185 | mode: request.mode,
186 | credentials: request.credentials,
187 | destination: request.destination,
188 | integrity: request.integrity,
189 | redirect: request.redirect,
190 | referrer: request.referrer,
191 | referrerPolicy: request.referrerPolicy,
192 | body,
193 | bodyUsed: request.bodyUsed,
194 | keepalive: request.keepalive,
195 | },
196 | })
197 |
198 | switch (clientMessage.type) {
199 | case 'MOCK_SUCCESS': {
200 | return delayPromise(
201 | () => respondWithMock(clientMessage),
202 | clientMessage.payload.delay,
203 | )
204 | }
205 |
206 | case 'MOCK_NOT_FOUND': {
207 | return getOriginalResponse()
208 | }
209 |
210 | case 'NETWORK_ERROR': {
211 | const { name, message } = clientMessage.payload
212 | const networkError = new Error(message)
213 | networkError.name = name
214 |
215 | // Rejecting a request Promise emulates a network error.
216 | throw networkError
217 | }
218 |
219 | case 'INTERNAL_ERROR': {
220 | const parsedBody = JSON.parse(clientMessage.payload.body)
221 |
222 | console.error(
223 | `\
224 | [MSW] Uncaught exception in the request handler for "%s %s":
225 |
226 | ${parsedBody.location}
227 |
228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
229 | `,
230 | request.method,
231 | request.url,
232 | )
233 |
234 | return respondWithMock(clientMessage)
235 | }
236 | }
237 |
238 | return getOriginalResponse()
239 | }
240 |
241 | self.addEventListener('fetch', function (event) {
242 | const { request } = event
243 | const accept = request.headers.get('accept') || ''
244 |
245 | // Bypass server-sent events.
246 | if (accept.includes('text/event-stream')) {
247 | return
248 | }
249 |
250 | // Bypass navigation requests.
251 | if (request.mode === 'navigate') {
252 | return
253 | }
254 |
255 | // Opening the DevTools triggers the "only-if-cached" request
256 | // that cannot be handled by the worker. Bypass such requests.
257 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
258 | return
259 | }
260 |
261 | // Bypass all requests when there are no active clients.
262 | // Prevents the self-unregistered worked from handling requests
263 | // after it's been deleted (still remains active until the next reload).
264 | if (activeClientIds.size === 0) {
265 | return
266 | }
267 |
268 | const requestId = uuidv4()
269 |
270 | return event.respondWith(
271 | handleRequest(event, requestId).catch((error) => {
272 | if (error.name === 'NetworkError') {
273 | console.warn(
274 | '[MSW] Successfully emulated a network error for the "%s %s" request.',
275 | request.method,
276 | request.url,
277 | )
278 | return
279 | }
280 |
281 | // At this point, any exception indicates an issue with the original request/response.
282 | console.error(
283 | `\
284 | [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
285 | request.method,
286 | request.url,
287 | `${error.name}: ${error.message}`,
288 | )
289 | }),
290 | )
291 | })
292 |
293 | function serializeHeaders(headers) {
294 | const reqHeaders = {}
295 | headers.forEach((value, name) => {
296 | reqHeaders[name] = reqHeaders[name]
297 | ? [].concat(reqHeaders[name]).concat(value)
298 | : value
299 | })
300 | return reqHeaders
301 | }
302 |
303 | function sendToClient(client, message) {
304 | return new Promise((resolve, reject) => {
305 | const channel = new MessageChannel()
306 |
307 | channel.port1.onmessage = (event) => {
308 | if (event.data && event.data.error) {
309 | return reject(event.data.error)
310 | }
311 |
312 | resolve(event.data)
313 | }
314 |
315 | client.postMessage(JSON.stringify(message), [channel.port2])
316 | })
317 | }
318 |
319 | function delayPromise(cb, duration) {
320 | return new Promise((resolve) => {
321 | setTimeout(() => resolve(cb()), duration)
322 | })
323 | }
324 |
325 | function respondWithMock(clientMessage) {
326 | return new Response(clientMessage.payload.body, {
327 | ...clientMessage.payload,
328 | headers: clientMessage.payload.headers,
329 | })
330 | }
331 |
332 | function uuidv4() {
333 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
334 | const r = (Math.random() * 16) | 0
335 | const v = c == 'x' ? r : (r & 0x3) | 0x8
336 | return v.toString(16)
337 | })
338 | }
339 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .sjs-client-app {
2 | color: #404040;
3 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | .sjs-client-app__logo {
7 | vertical-align: middle;
8 | }
9 |
10 | .sjs-client-app__header {
11 | background-color: var(--primary, #19b394);
12 | padding: 10px;
13 | padding-bottom: 0;
14 | }
15 |
16 | .sjs-nav-button {
17 | display: inline-block;
18 | margin-left: 24px;
19 | font-weight: bold;
20 | font-size: 22px;
21 | text-decoration: none;
22 | line-height: 40px;
23 | color: white;
24 | }
25 |
26 | .sjs-nav-button:hover,
27 | .sjs-nav-button.active {
28 | text-decoration: underline;
29 | }
30 |
31 | .sjs-client-app__content {
32 | position: fixed;
33 | top: 64px;
34 | width: 100%;
35 | left: 0;
36 | bottom: 0;
37 | overflow: auto;
38 | user-select: none;
39 | }
40 |
41 | h1 {
42 | padding-left: 24px;
43 | }
44 |
45 | .sjs-client-app__content--about h1,
46 | .sjs-client-app__content--surveys-list h1 {
47 | padding: 0;
48 | }
49 |
50 | .sjs-client-app__content--surveys-list,
51 | .sjs-client-app__content--about {
52 | max-width: 800px;
53 | margin: 0 auto;
54 | }
55 |
56 | .sjs-client-app__footer {}
57 |
58 | .sjs-editor-container {
59 | height: calc(100% - 2px);
60 | }
61 |
62 | .sjs-results-container {
63 | height: calc(100% - 88px);
64 | }
65 |
66 | .sjs-results-content {
67 | height: 100%;
68 | }
69 |
70 | .sjs-results-placeholder {
71 | line-height: 200px;
72 | text-align: center;
73 | border: 1px dotted lightgray;
74 | }
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 | import { load } from './redux/surveys'
5 | import store from './redux';
6 |
7 | test('renders home page header', () => {
8 | render();
9 | const linkElement = screen.getByText(/My Surveys/i, { selector: 'span' });
10 | expect(linkElement).toBeInTheDocument();
11 | const headerElement = screen.getByText(/My Surveys/i, { selector: 'h1' });
12 | expect(headerElement).toBeInTheDocument();
13 | });
14 |
15 | test('get surveys list', async () => {
16 | const loadResult = await store.dispatch(load())
17 | const surveys = loadResult.payload
18 | expect(surveys.length).toBe(2)
19 | expect(surveys[0].name).toBe('Product Feedback Survey')
20 | expect(surveys[1].name).toBe('Customer and their partner income survey')
21 | });
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { BrowserRouter as Router } from "react-router-dom";
4 | import Content, { NavBar } from './routes'
5 | import store from './redux';
6 | import './App.css';
7 | import logo from './logo.svg';
8 |
9 | function App() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default App;
30 |
--------------------------------------------------------------------------------
/src/components/Editor.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react'
2 | import { useReduxDispatch } from '../redux'
3 | import { get, update } from '../redux/surveys'
4 | import { SurveyCreator, SurveyCreatorComponent } from 'survey-creator-react'
5 | import 'survey-creator-core/survey-creator-core.css'
6 |
7 | const Editor = (params: { id: string }): React.ReactElement => {
8 | const dispatch = useReduxDispatch()
9 | const creator = useMemo(() => {
10 | const options = {
11 | showLogicTab: true,
12 | showThemeTab: true,
13 | showTranslationTab: true
14 | };
15 | return new SurveyCreator(options);
16 | }, []);
17 | creator.isAutoSave = true;
18 | creator.saveSurveyFunc = (saveNo: number, callback: (no: number, success: boolean) => void) => {
19 | dispatch(update({ id: params.id, json: creator.JSON, text: creator.text }))
20 | callback(saveNo, true);
21 | }
22 |
23 | useEffect(() => {
24 | (async () => {
25 | const surveyAction = await dispatch(get(params.id))
26 | if(typeof surveyAction.payload.json === 'object') {
27 | creator.JSON = surveyAction.payload.json
28 | } else {
29 | creator.text = surveyAction.payload.json
30 | }
31 | })()
32 | }, [dispatch, creator, params.id])
33 |
34 | return (<>
35 |
36 | >)
37 | }
38 |
39 | export default Editor
--------------------------------------------------------------------------------
/src/components/Surveys.css:
--------------------------------------------------------------------------------
1 | .sjs-surveys-list {
2 | font-size: 16px;
3 | line-height: 40px;
4 | width: 100%;
5 | table-layout: fixed;
6 | }
7 |
8 | .sjs-surveys-list__footer {
9 | margin: 48px auto;
10 | /* max-width: 500px; */
11 | }
12 |
13 | .sjs-button {
14 | appearance: none;
15 | -webkit-appearance: none;
16 | text-decoration: none;
17 | outline: none;
18 | border-radius: 100px;
19 | background: transparent;
20 | padding: 8px 24px;
21 | border: none;
22 | color: var(--primary, #19b394);
23 | cursor: pointer;
24 | font-weight: 600;
25 | text-align: center;
26 | }
27 |
28 | .sjs-button:hover {
29 | /* background-color: var(--primary, #19b394); */
30 | background-color: var(--primary-light, rgba(25, 179, 148, 0.1));
31 | }
32 |
33 | .sjs-add-btn {
34 | display: inline-block;
35 | padding: 16px 48px;
36 | background: white;
37 | box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.15);
38 | border-radius: 4px;
39 | text-align: center;
40 | border: 2px solid transparent;
41 | width: 100%;
42 | box-sizing: border-box;
43 | }
44 |
45 | .sjs-add-btn:hover,
46 | .sjs-add-btn:focus {
47 | border-color: var(--primary, #19b394);
48 | background-color: white;
49 | }
50 |
51 | .sjs-remove-btn {
52 | color: var(--danger, #e60a3e);
53 | }
54 |
55 | .sjs-remove-btn:hover,
56 | .sjs-remove-btn:focus {
57 | /* background-color: var(--danger, #e60a3e); */
58 | background-color: var(--danger-light, rgba(230, 10, 62, 0.1));
59 | }
--------------------------------------------------------------------------------
/src/components/Surveys.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import { create, load, remove } from '../redux/surveys'
3 | import { useReduxDispatch, useReduxSelector } from '../redux'
4 | import { Link } from 'react-router-dom'
5 | import './Surveys.css'
6 |
7 | const Surveys = (): React.ReactElement => {
8 | const surveys = useReduxSelector(state => state.surveys.surveys)
9 | const dispatch = useReduxDispatch()
10 |
11 | const postStatus = useReduxSelector(state => state.surveys.status)
12 |
13 | useEffect(() => {
14 | if (postStatus === 'idle') {
15 | dispatch(load())
16 | }
17 | }, [postStatus, dispatch])
18 |
19 | return (<>
20 |
21 | {surveys.map(survey =>
22 |
23 | {survey.name} |
24 |
25 | Run
26 | Edit
27 | Results
28 | dispatch(remove(survey.id))}>Remove
29 | |
30 |
31 | )}
32 |
33 |
34 | dispatch(create())}>Add Survey
35 |
36 | >)
37 | }
38 |
39 | export default Surveys
--------------------------------------------------------------------------------
/src/components/Viewer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 | import { useReduxDispatch } from '../redux'
3 | import { load } from '../redux/results'
4 | import { get } from '../redux/surveys'
5 | import { Model } from 'survey-core'
6 | import 'tabulator-tables/dist/css/tabulator.css'
7 | import 'survey-analytics/survey.analytics.tabulator.css'
8 | const SurveyAnalyticsTabulator = require('survey-analytics/survey.analytics.tabulator')
9 |
10 | const Viewer = (params: { id: string }): React.ReactElement => {
11 | const visContainerRef = useRef(null);
12 | const dispatch = useReduxDispatch()
13 |
14 | useEffect(() => {
15 | (async () => {
16 | const surveyAction = await dispatch(get(params.id))
17 | const survey = surveyAction.payload
18 | const resultsAction = await dispatch(load(params.id))
19 | const data = resultsAction.payload.data
20 | if (data.length > 0 && visContainerRef.current) {
21 | var model = new Model(survey.json);
22 | visContainerRef.current.innerHTML = '';
23 | var surveyAnalyticsTabulator = new SurveyAnalyticsTabulator.Tabulator(
24 | model,
25 | data.map((item: any) => typeof item === 'string' ? JSON.parse(item) : item)
26 | );
27 | surveyAnalyticsTabulator.render(visContainerRef.current);
28 | }
29 | })()
30 | }, [dispatch, params.id])
31 |
32 | return (<>
33 |
34 |
35 | This survey doesn't have any answers yet
36 |
37 |
38 | >)
39 | }
40 |
41 | export default Viewer
--------------------------------------------------------------------------------
/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.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | if (process.env.NODE_ENV === 'development') {
8 | const { worker } = require('./mocks/browser')
9 | worker.start()
10 | }
11 |
12 | ReactDOM.render(
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 |
19 | // If you want to start measuring performance in your app, pass a function
20 | // to log results (for example: reportWebVitals(console.log))
21 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
22 | reportWebVitals();
23 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
33 |
--------------------------------------------------------------------------------
/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw'
2 | import { handlers } from './handlers'
3 |
4 | // This configures a Service Worker with the given request handlers.
5 | export const worker = setupWorker(...handlers)
--------------------------------------------------------------------------------
/src/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { rest } from 'msw'
2 | import { createSurvey, getResults, getSurvey, getSurveys, postResult, removeSurvey, updateSurvey } from '../models/in-memory-storage'
3 | import { apiBaseAddress } from '../models/survey'
4 |
5 | export const handlers = [
6 | rest.get(apiBaseAddress + '/surveys', (req, res, ctx) => {
7 | // const { userId } = req.params
8 | // return res(
9 | // ctx.json({
10 | // id: userId,
11 | // firstName: 'John',
12 | // lastName: 'Maverick',
13 | // }),
14 | // )
15 | return res(
16 | ctx.json(getSurveys()),
17 | )
18 | }),
19 | rest.get(apiBaseAddress + '/getActive', (req, res, ctx) => {
20 | return res(
21 | ctx.json(getSurveys()),
22 | )
23 | }),
24 | rest.get(apiBaseAddress + '/create', (req, res, ctx) => {
25 | return res(
26 | ctx.json(createSurvey()),
27 | )
28 | }),
29 | rest.get(apiBaseAddress + '/delete', (req, res, ctx) => {
30 | const id = req.url.searchParams.get('id')
31 | removeSurvey(id as string);
32 | return res(
33 | ctx.json({ id }),
34 | )
35 | }),
36 | rest.get(apiBaseAddress + '/getSurvey', (req, res, ctx) => {
37 | const surveyId = req.url.searchParams.get('surveyId')
38 | return res(
39 | ctx.json(getSurvey(surveyId as string)),
40 | )
41 | }),
42 | rest.post(apiBaseAddress + '/changeJson', (req, res, ctx) => {
43 | const { id, json } = req.body as Record
44 | updateSurvey(id as string, json)
45 | return res(
46 | ctx.json({ id, json }),
47 | )
48 | }),
49 | rest.post(apiBaseAddress + '/post', (req, res, ctx) => {
50 | const { postId, surveyResult } = req.body as Record
51 | postResult(postId as string, surveyResult)
52 | return res(
53 | ctx.json({}),
54 | )
55 | }),
56 | rest.get(apiBaseAddress + '/results', (req, res, ctx) => {
57 | const postId = req.url.searchParams.get('postId')
58 | return res(
59 | ctx.json({ id: postId, data: getResults(postId as string) }),
60 | )
61 | })
62 | ]
--------------------------------------------------------------------------------
/src/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from 'msw/node'
2 | import { handlers } from './handlers'
3 |
4 | export const server = setupServer(...handlers)
--------------------------------------------------------------------------------
/src/models/in-memory-storage.ts:
--------------------------------------------------------------------------------
1 | import { defaultJSON, ISurveyDefinition, survey1Json, survey1Results, survey2Json, survey2Results } from "./survey";
2 |
3 | const surveys: Array = [ survey1Json, survey2Json ];
4 |
5 | const results: { [index: string]: Array } = {
6 | '1': survey1Results,
7 | '2': survey2Results
8 | };
9 |
10 | let nextId = 3;
11 |
12 | export function getSurveys() {
13 | return ([] as Array).concat(surveys);
14 | }
15 |
16 | export function createSurvey() {
17 | let newSurvey = JSON.parse(JSON.stringify(defaultJSON));
18 | newSurvey.id = '' + nextId++
19 | newSurvey.name += ' ' + newSurvey.id;
20 | surveys.push(newSurvey);
21 | return newSurvey;
22 | }
23 |
24 | export function getSurvey(id: string) {
25 | return surveys.filter(s => s.id === id)[0];
26 | }
27 |
28 | export function removeSurvey(id: string) {
29 | const survey = surveys.filter(s => s.id === id)[0];
30 | const index = surveys.indexOf(survey);
31 | if(index >= 0) {
32 | surveys.splice(index, 1);
33 | }
34 | }
35 |
36 | export function updateSurvey(id: string, json: any) {
37 | const survey = surveys.filter(s => s.id === id)[0];
38 | survey.json = json;
39 | }
40 |
41 | export function postResult(id: string, json: any) {
42 | if(!Array.isArray(results[id])) {
43 | results[id] = [];
44 | }
45 | results[id].push(json);
46 | }
47 |
48 | export function getResults(id: string) {
49 | return results[id] || [];
50 | }
51 |
--------------------------------------------------------------------------------
/src/models/survey.ts:
--------------------------------------------------------------------------------
1 | export interface ISurveyDefinition {
2 | id: string,
3 | name: string,
4 | json: any
5 | }
6 |
7 | export const defaultJSON = {
8 | id: '',
9 | name: 'New Survey',
10 | json: {
11 | elements: [
12 | { type: 'radiogroup', name: 'question1', choices: [ '1', '2', '3' ] }
13 | ]
14 | }
15 | }
16 |
17 | export const survey1Json = {
18 | id: "1",
19 | name: "Product Feedback Survey",
20 | json: {
21 | pages: [{
22 | elements: [{
23 | type: "matrix",
24 | name: "Quality",
25 | title: "Please indicate if you agree or disagree with the following statements",
26 | columns: [{
27 | value: 1,
28 | text: "Strongly disagree"
29 | }, {
30 | value: 2,
31 | text: "Disagree"
32 | }, {
33 | value: 3,
34 | text: "Neutral"
35 | }, {
36 | value: 4,
37 | text: "Agree"
38 | }, {
39 | value: 5,
40 | text: "Strongly agree"
41 | }],
42 | rows: [{
43 | value: "affordable",
44 | text: "Product is affordable"
45 | }, {
46 | value: "does what it claims",
47 | text: "Product does what it claims"
48 | }, {
49 | value: "better then others",
50 | text: "Product is better than other products on the market"
51 | }, {
52 | value: "easy to use",
53 | text: "Product is easy to use"
54 | }]
55 | }, {
56 | type: "rating",
57 | name: "satisfaction",
58 | title: "How satisfied are you with the product?",
59 | mininumRateDescription: "Not satisfied",
60 | maximumRateDescription: "Completely satisfied"
61 | }, {
62 | type: "rating",
63 | name: "recommend friends",
64 | visibleIf: "{satisfaction} > 3",
65 | title: "How likely are you to recommend the product to a friend or colleague?",
66 | mininumRateDescription: "Won't recommend",
67 | maximumRateDescription: "Will recommend"
68 | }, {
69 | type: "comment",
70 | name: "suggestions",
71 | title: "What would make you more satisfied with the product?"
72 | }]
73 | }, {
74 | elements: [{
75 | type: "radiogroup",
76 | name: "price to competitors",
77 | title: "Compared to our competitors, do you feel the product is",
78 | choices: [
79 | "Less expensive",
80 | "Priced about the same",
81 | "More expensive",
82 | "Not sure"
83 | ]
84 | }, {
85 | type: "radiogroup",
86 | name: "price",
87 | title: "Do you feel our current price is merited by our product?",
88 | choices: [
89 | "correct|Yes, the price is about right",
90 | "low|No, the price is too low",
91 | "high|No, the price is too high"
92 | ]
93 | }, {
94 | type: "multipletext",
95 | name: "pricelimit",
96 | title: "What is the... ",
97 | items: [{
98 | name: "mostamount",
99 | title: "Most amount you would pay for a product like ours"
100 | }, {
101 | name: "leastamount",
102 | title: "The least amount you would feel comfortable paying"
103 | }]
104 | }]
105 | }, {
106 | elements: [{
107 | type: "text",
108 | name: "email",
109 | title: 'Thank you for taking our survey. Please enter your email address and press the "Submit" button.'
110 | }]
111 | }]
112 | }
113 | };
114 |
115 | export const survey1Results = [
116 | {'Quality':{'affordable':'5','better then others':'5','does what it claims':'5','easy to use':'5'},'satisfaction':5,'recommend friends':5,'suggestions':'I am happy!','price to competitors':'Not sure','price':'low','pricelimit':{'mostamount':'100','leastamount':'100'}},
117 | {'Quality':{'affordable':'3','does what it claims':'2','better then others':'2','easy to use':'3'},'satisfaction':3,'suggestions':'better support','price to competitors':'Not sure','price':'high','pricelimit':{'mostamount':'60','leastamount':'10'}}
118 | ];
119 |
120 | export const survey2Json = {
121 | id: "2",
122 | name: "Customer and their partner income survey",
123 | json: {
124 | completeText: "Finish",
125 | pageNextText: "Continue",
126 | pagePrevText: "Previous",
127 | pages: [{
128 | elements: [{
129 | type: "panel",
130 | elements: [{
131 | type: "html",
132 | name: "income_intro",
133 | html:
134 | "Income. In this section, you will be asked about your current employment status and other ways you and your partner receive income. It will be handy to have the following in front of you: payslip (for employment details), latest statement from any payments (from Centrelink or other authority), a current Centrelink Schedule for any account-based pension from super, annuities, or other income stream products that you may own. If you don't have a current one, you can get these schedules by contacting your income stream provider."
135 | }],
136 | name: "panel1"
137 | }],
138 | name: "page0"
139 | }, {
140 | elements: [{
141 | type: "panel",
142 | elements: [{
143 | type: "radiogroup",
144 | choices: [
145 | "Married",
146 | "In a registered relationship",
147 | "Living with my partner",
148 | "Widowed",
149 | "Single"
150 | ],
151 | name: "maritalstatus_c",
152 | title: " "
153 | }],
154 | name: "panel13",
155 | title: "What is your marital status?"
156 | }],
157 | name: "page1"
158 | }, {
159 | elements: [{
160 | type: "panel",
161 | elements: [{
162 | type: "panel",
163 | elements: [{
164 | type: "radiogroup",
165 | choices: [{
166 | value: "1",
167 | text: "Yes"
168 | }, {
169 | value: "0",
170 | text: "No"
171 | }],
172 | colCount: 2,
173 | isRequired: true,
174 | name: "member_receives_income_from_employment",
175 | title: " "
176 | }, {
177 | type: "checkbox",
178 | name: "member_type_of_employment",
179 | visible: false,
180 | visibleIf: "{member_receives_income_from_employment} =1",
181 | title: " ",
182 | isRequired: true,
183 | choices: [
184 | "Self-employed",
185 | "Other types of employment"
186 | ]
187 | }],
188 | name: "panel2",
189 | title: "You"
190 | }, {
191 | type: "panel",
192 | elements: [{
193 | type: "radiogroup",
194 | choices: [{
195 | value: "1",
196 | text: "Yes"
197 | }, {
198 | value: "0",
199 | text: "No"
200 | }],
201 | colCount: 2,
202 | isRequired: true,
203 | name: "partner_receives_income_from_employment",
204 | title: " "
205 | }, {
206 | type: "checkbox",
207 | name: "partner_type_of_employment",
208 | visible: false,
209 | visibleIf: "{partner_receives_income_from_employment} =1",
210 | title: " ",
211 | isRequired: true,
212 | choices: [
213 | "Self-employed",
214 | "Other types of employment"
215 | ]
216 | }],
217 | name: "panel1",
218 | startWithNewLine: false,
219 | title: "Your Partner",
220 | visibleIf:
221 | "{maritalstatus_c} = 'Married' or {maritalstatus_c} = 'In a registered relationship' or {maritalstatus_c} = 'Living with my partner'"
222 | }],
223 | name: "panel5",
224 | title: "Do you and/or your partner currently receive income from employment?"
225 | }],
226 | name: "page2"
227 | }, {
228 | elements: [{
229 | type: "panel",
230 | elements: [{
231 | type: "panel",
232 | elements: [{
233 | type: "paneldynamic",
234 | minPanelCount: 1,
235 | name: "member_array_employer_names",
236 | valueName: "member_array_employer",
237 | title: "Enter information about your employers",
238 | panelAddText: "Add another employer",
239 | panelCount: 1,
240 | templateElements: [{
241 | type: "text",
242 | name: "member_employer_name",
243 | valueName: "name",
244 | title: "Employer name"
245 | }]
246 | }],
247 | name: "panel2",
248 | title: "You",
249 | visible: false,
250 | visibleIf: "{member_type_of_employment} contains 'Other types of employment'"
251 | }, {
252 | type: "panel",
253 | elements: [{
254 | type: "paneldynamic",
255 | minPanelCount: 1,
256 | name: "partner_array_employer_names",
257 | valueName: "partner_array_employer",
258 | title: "Enter information about employers of your partner",
259 | panelAddText: "Add another employer",
260 | panelCount: 1,
261 | templateElements: [{
262 | type: "text",
263 | name: "partner_employer_name",
264 | valueName: "name",
265 | title: "Employer name"
266 | }]
267 | }],
268 | name: "panel8",
269 | startWithNewLine: false,
270 | title: "Your Partner",
271 | visible: false,
272 | visibleIf:
273 | "{partner_type_of_employment} contains 'Other types of employment'"
274 | }],
275 | name: "panel6",
276 | title: "Employers"
277 | }],
278 | name: "page3.1",
279 | visible: false,
280 | visibleIf:
281 | "{member_type_of_employment} contains 'Other types of employment' or {partner_type_of_employment} contains 'Other types of employment'"
282 | }, {
283 | elements: [{
284 | type: "panel",
285 | elements: [{
286 | type: "panel",
287 | elements: [{
288 | type: "paneldynamic",
289 | renderMode: "progressTop",
290 | allowAddPanel: false,
291 | allowRemovePanel: false,
292 | name: "member_array_employer_info",
293 | title: "Your employers",
294 | valueName: "member_array_employer",
295 | panelCount: 1,
296 | templateElements: [{
297 | type: "panel",
298 | name: "panel_member_employer_address",
299 | title: "Contacts",
300 | elements: [{
301 | type: "text",
302 | name: "member_employer_address",
303 | valueName: "address",
304 | title: "Address:"
305 | }, {
306 | type: "text",
307 | name: "member_employer_phone",
308 | valueName: "phone",
309 | title: "Phone number:"
310 | }, {
311 | type: "text",
312 | name: "member_employer_abn",
313 | valueName: "abn",
314 | title: "ABN:"
315 | }]
316 | }, {
317 | type: "panel",
318 | name: "panel_member_employer_role",
319 | title: "Are you a full time worker?",
320 | elements: [{
321 | type: "radiogroup",
322 | choices: [
323 | "Full-time",
324 | "Part-time",
325 | "Casual",
326 | "Seasonal"
327 | ],
328 | name: "member_employer_role",
329 | title: " ",
330 | valueName: "role"
331 | }]
332 | }, {
333 | type: "panel",
334 | name: "panel_member_employer_hours_work",
335 | title: "How many hours do you work?",
336 | elements: [{
337 | type: "text",
338 | inputType: "number",
339 | name: "member_employer_hours_worked",
340 | valueName: "hours_worked",
341 | title: "Hours:"
342 | }, {
343 | type: "dropdown",
344 | name: "member_employer_hours_worked_frequency",
345 | title: "Work frequency:",
346 | valueName: "hours_worked_frequency",
347 | startWithNewLine: false,
348 | defaultValue: "Day",
349 | choices: [
350 | "Day",
351 | "Week",
352 | "Fortnight",
353 | "Month",
354 | "Year"
355 | ]
356 | }]
357 | }, {
358 | type: "panel",
359 | name: "panel_member_employer_income",
360 | title: "What is your income?",
361 | elements: [{
362 | type: "text",
363 | inputType: "number",
364 | name: "member_employer_income",
365 | valueName: "income",
366 | title: "Income:"
367 | }, {
368 | type: "dropdown",
369 | name: "member_employer_income_frequency",
370 | title: "Income frequency:",
371 | valueName: "income_frequency",
372 | startWithNewLine: false,
373 | defaultValue: "Month",
374 | choices: [
375 | "Day",
376 | "Week",
377 | "Fortnight",
378 | "Month",
379 | "Year"
380 | ]
381 | }]
382 | }],
383 | templateTitle: "Employer name: {panel.name}"
384 | }],
385 | name: "panel17",
386 | title: "You",
387 | visibleIf: "{member_type_of_employment} contains 'Other types of employment'"
388 | }, {
389 | type: "panel",
390 | elements: [{
391 | type: "paneldynamic",
392 | renderMode: "progressTop",
393 | allowAddPanel: false,
394 | allowRemovePanel: false,
395 | name: "partner_array_employer_info",
396 | title: "Employers",
397 | valueName: "partner_array_employer",
398 | panelCount: 1,
399 | templateElements: [{
400 | type: "panel",
401 | name: "panel_partner_employer_address",
402 | title: "Contacts",
403 | elements: [{
404 | type: "text",
405 | name: "partner_employer_address",
406 | valueName: "address",
407 | title: "Address:"
408 | }, {
409 | type: "text",
410 | name: "partner_employer_phone",
411 | valueName: "phone",
412 | title: "Phone number:"
413 | }, {
414 | type: "text",
415 | name: "partner_employer_abn",
416 | valueName: "abn",
417 | title: "ABN:"
418 | }]
419 | }, {
420 | type: "panel",
421 | name: "panel_partner_employer_role",
422 | title: "Are you a full time worker?",
423 | elements: [{
424 | type: "radiogroup",
425 | choices: [
426 | "Full-time",
427 | "Part-time",
428 | "Casual",
429 | "Seasonal"
430 | ],
431 | name: "partner_employer_role",
432 | title: "Your role",
433 | valueName: "role"
434 | }]
435 | }, {
436 | type: "panel",
437 | name: "panel_partner_employer_hours_work",
438 | title: "How many hours do you work?",
439 | elements: [{
440 | type: "text",
441 | inputType: "number",
442 | name: "partner_employer_hours_worked",
443 | valueName: "hours_worked",
444 | title: "Hours:"
445 | }, {
446 | type: "dropdown",
447 | name: "partner_employer_hours_worked_frequency",
448 | valueName: "hours_worked_frequency",
449 | title: "Work frequency:",
450 | startWithNewLine: false,
451 | defaultValue: "Day",
452 | choices: [
453 | "Day",
454 | "Week",
455 | "Fortnight",
456 | "Month",
457 | "Year"
458 | ]
459 | }]
460 | }, {
461 | type: "panel",
462 | name: "panel_partner_employer_income",
463 | title: "What is your income?",
464 | elements: [{
465 | type: "text",
466 | inputType: "number",
467 | name: "partner_employer_income",
468 | valueName: "income",
469 | title: "Income:"
470 | }, {
471 | type: "dropdown",
472 | name: "partner_employer_income_frequency",
473 | valueName: "income_frequency",
474 | title: "Income frequency:",
475 | startWithNewLine: false,
476 | defaultValue: "Month",
477 | choices: [
478 | "Day",
479 | "Week",
480 | "Fortnight",
481 | "Month",
482 | "Year"
483 | ]
484 | }]
485 | }],
486 | templateTitle: "Employer name: {panel.name}"
487 | }],
488 | name: "panel18",
489 | startWithNewLine: false,
490 | title: "You partner",
491 | visibleIf: "{partner_type_of_employment} contains 'Other types of employment'"
492 | }],
493 | name: "panel16",
494 | title: "Enter information about your employers"
495 | }],
496 | name: "page3.2",
497 | visibleIf:
498 | "{member_type_of_employment} contains 'Other types of employment' or {partner_type_of_employment} contains 'Other types of employment'"
499 | }, {
500 | elements: [{
501 | type: "panel",
502 | elements: [{
503 | type: "panel",
504 | elements: [{
505 | type: "radiogroup",
506 | choices: [{
507 | value: "1",
508 | text: "Yes"
509 | }, {
510 | value: "0",
511 | text: "No"
512 | }],
513 | colCount: 2,
514 | isRequired: true,
515 | name: "member_receive_fringe_benefits",
516 | title: " "
517 | }, {
518 | type: "panel",
519 | elements: [{
520 | type: "text",
521 | name: "member_fringe_benefits_type"
522 | }, {
523 | type: "text",
524 | name: "member_fringe_benefits_value"
525 | }, {
526 | type: "radiogroup",
527 | choices: ["Grossed up", "Not grossed up"],
528 | name: "member_fringe_benefits_grossing"
529 | }],
530 | name: "panel11",
531 | visible: false,
532 | visibleIf: "{member_receive_fringe_benefits} = 1"
533 | }],
534 | name: "panel2",
535 | title: "You",
536 | visible: false,
537 | visibleIf: "{member_type_of_employment} contains 'Other types of employment'"
538 | }, {
539 | type: "panel",
540 | elements: [{
541 | type: "radiogroup",
542 | choices: [{
543 | value: "1",
544 | text: "Yes"
545 | }, {
546 | value: "0",
547 | text: "No"
548 | }],
549 | colCount: 2,
550 | isRequired: true,
551 | name: "partner_receive_fringe_benefits",
552 | title: " "
553 | }, {
554 | type: "panel",
555 | elements: [{
556 | type: "text",
557 | name: "partner_fringe_benefits_type"
558 | }, {
559 | type: "text",
560 | name: "partner_fringe_benefits_value"
561 | }, {
562 | type: "radiogroup",
563 | choices: ["Grossed up", "Not grossed up"],
564 | name: "partner_fringe_benefits_grossing"
565 | }],
566 | name: "panel12",
567 | visible: false,
568 | visibleIf: "{partner_receive_fringe_benefits} = 1"
569 | }],
570 | name: "panel1",
571 | startWithNewLine: false,
572 | title: "Your Partner",
573 | visible: false,
574 | visibleIf: "{partner_type_of_employment} contains 'Other types of employment'"
575 | }],
576 | name: "panel9",
577 | title: "Do any of your employers provide you with fringe benefits?"
578 | }],
579 | name: "page4",
580 | visible: false,
581 | visibleIf:
582 | "{member_type_of_employment} contains 'Other types of employment' or {partner_type_of_employment} contains 'Other types of employment'"
583 | }, {
584 | elements: [{
585 | type: "panel",
586 | elements: [{
587 | type: "panel",
588 | elements: [{
589 | type: "radiogroup",
590 | choices: [{
591 | value: "1",
592 | text: "Yes"
593 | }, {
594 | value: "0",
595 | text: "No"
596 | }],
597 | colCount: 2,
598 | isRequired: true,
599 | name: "member_seasonal_intermittent_or_contract_work",
600 | title: " "
601 | }],
602 | name: "panel2",
603 | title: "You",
604 | visible: false,
605 | visibleIf: "{member_receives_income_from_employment} = 1"
606 | }, {
607 | type: "panel",
608 | elements: [{
609 | type: "radiogroup",
610 | choices: [{
611 | value: "1",
612 | text: "Yes"
613 | }, {
614 | value: "0",
615 | text: "No"
616 | }],
617 | colCount: 2,
618 | isRequired: true,
619 | name: "partner_seasonal_intermittent_or_contract_work",
620 | title: " "
621 | }],
622 | name: "panel1",
623 | startWithNewLine: false,
624 | title: "Your Partner",
625 | visible: false,
626 | visibleIf: "{partner_receives_income_from_employment} =1 "
627 | }],
628 | name: "panel10",
629 | title: "In the last 6 months, have you done any seasonal, intermittent or contract work?"
630 | }],
631 | name: "page5",
632 | visible: false,
633 | visibleIf: "{member_receives_income_from_employment} = 1 or {partner_receives_income_from_employment} =1 "
634 | }],
635 | requiredText: "",
636 | showQuestionNumbers: "off",
637 | storeOthersAsComment: false
638 | }
639 | };
640 |
641 | export const survey2Results = [
642 | {'member_array_employer':[{}],'partner_array_employer':[{}],'maritalstatus_c':'Married','member_receives_income_from_employment':'0','partner_receives_income_from_employment':'0'},
643 | {'member_array_employer':[{}],'partner_array_employer':[{}],'maritalstatus_c':'Single','member_receives_income_from_employment':'1','member_type_of_employment':['Self employment'],'member_seasonal_intermittent_or_contract_work':'0'}
644 | ];
645 |
646 | export var apiBaseAddress = '/api';
--------------------------------------------------------------------------------
/src/pages/About.tsx:
--------------------------------------------------------------------------------
1 | const About = () => (
2 |
About
3 |
4 | This React application demonstrates how to implement a basic SurveyJS service client. You can use it with any backend that supports REST API.
5 |
6 |
)
7 |
8 | export default About;
--------------------------------------------------------------------------------
/src/pages/Edit.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router'
2 | import Editor from '../components/Editor'
3 |
4 | const Edit = () => {
5 | const { id } = useParams();
6 | return (<>
7 |
8 |
9 |
10 | >);
11 | }
12 |
13 | export default Edit;
--------------------------------------------------------------------------------
/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import Surveys from '../components/Surveys'
2 |
3 | const Home = () => (
4 |
5 |
My Surveys
6 |
7 |
8 | )
9 |
10 | export default Home;
--------------------------------------------------------------------------------
/src/pages/Results.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router'
2 | import { useReduxSelector } from '../redux';
3 | import Viewer from '../components/Viewer'
4 |
5 | const Results = () => {
6 | const { id } = useParams();
7 | const surveys = useReduxSelector(state => state.surveys.surveys)
8 | const survey = surveys.filter(s => s.id === id)[0]
9 | return (<>
10 | {'\'' + survey.name + '\' results'}
11 |
12 |
13 |
14 | >);
15 | }
16 |
17 | export default Results;
--------------------------------------------------------------------------------
/src/pages/Run.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from 'react-router'
2 | import { useReduxDispatch, useReduxSelector } from '../redux'
3 | import { post } from '../redux/results'
4 | import { Model } from 'survey-core'
5 | import { Survey } from 'survey-react-ui'
6 | import 'survey-core/survey-core.css'
7 |
8 | const Run = () => {
9 | const { id } = useParams();
10 | const dispatch = useReduxDispatch()
11 | const surveys = useReduxSelector(state => state.surveys.surveys)
12 | const survey = surveys.filter(s => s.id === id)[0]
13 | const model = new Model(survey.json)
14 |
15 | model
16 | .onComplete
17 | .add((sender: Model) => {
18 | dispatch(post({postId: id as string, surveyResult: sender.data, surveyResultText: JSON.stringify(sender.data)}))
19 | });
20 |
21 | return (<>
22 | {survey.name}
23 |
24 | >);
25 | }
26 |
27 | export default Run;
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/redux/index.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 | import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'
3 | import { createBrowserHistory } from 'history'
4 | import rootReducer from './root-reducer'
5 |
6 | export const history = createBrowserHistory()
7 |
8 | const store = configureStore({
9 | reducer: rootReducer(history),
10 | // middleware: getDefaultMiddleware => getDefaultMiddleware(), // .prepend(middleware)
11 | })
12 |
13 | export type RootState = ReturnType
14 |
15 | export type AppDispatch = typeof store.dispatch
16 | export const useReduxDispatch = (): AppDispatch => useDispatch()
17 | export const useReduxSelector: TypedUseSelectorHook = useSelector
18 | export default store
--------------------------------------------------------------------------------
/src/redux/results.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from '@reduxjs/toolkit'
2 | import axios from 'axios'
3 | import { apiBaseAddress } from '../models/survey'
4 |
5 | export const load = createAsyncThunk('results/load', async (id: string) => {
6 | const response = await axios.get(apiBaseAddress + '/results?postId=' + id)
7 | return response.data
8 | })
9 |
10 | export const post = createAsyncThunk('results/post', async (data: {postId: string, surveyResult: any, surveyResultText: string}) => {
11 | const response = await axios.post(apiBaseAddress + '/post', data);
12 | return response.data
13 | })
14 |
--------------------------------------------------------------------------------
/src/redux/root-reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from '@reduxjs/toolkit'
2 | import { connectRouter } from 'connected-react-router'
3 | import { History } from 'history'
4 | import surveysReducer from './surveys'
5 |
6 | const rootReducer = (history: History) =>
7 | combineReducers({
8 | surveys: surveysReducer,
9 | router: connectRouter(history),
10 | })
11 |
12 | export default rootReducer
--------------------------------------------------------------------------------
/src/redux/surveys.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
2 | import axios from 'axios'
3 | import { apiBaseAddress, ISurveyDefinition } from '../models/survey'
4 |
5 | const initialState: { surveys: Array, status: string, error: any } = {
6 | surveys: [],
7 | status: 'idle',
8 | error: null
9 | }
10 |
11 | const surveysSlice = createSlice({
12 | name: 'surveys',
13 | initialState,
14 | reducers: {
15 | // add: (state, action: PayloadAction) => {
16 | // state.surveys.push(getDefaultJSON());
17 | // },
18 | // remove: (state, action: PayloadAction) => {
19 | // const survey = state.surveys.filter(s => s.id === action.payload)[0];
20 | // const index = state.surveys.indexOf(survey);
21 | // if(index >= 0) {
22 | // state.surveys.splice(index, 1);
23 | // }
24 | // },
25 | // update: (state, action: PayloadAction<{id: string, json: any}>) => {
26 | // const survey = state.surveys.filter(s => s.id === action.payload.id)[0];
27 | // survey.json = action.payload.json;
28 | // },
29 | },
30 | extraReducers(builder) {
31 | builder
32 | .addCase(load.pending, (state, action) => {
33 | state.status = 'loading'
34 | })
35 | .addCase(load.fulfilled, (state, action) => {
36 | state.status = 'succeeded'
37 | // Add any fetched surveys to the array
38 | state.surveys = state.surveys.concat(action.payload)
39 | })
40 | .addCase(load.rejected, (state, action) => {
41 | state.status = 'failed'
42 | state.error = action.error.message
43 | })
44 | .addCase(create.fulfilled, (state, action) => {
45 | state.status = 'succeeded'
46 | // Add new survey to the array
47 | state.surveys.push(action.payload)
48 | })
49 | .addCase(remove.fulfilled, (state, action) => {
50 | state.status = 'succeeded'
51 | // Remove survey from the array
52 | const survey = state.surveys.filter(s => s.id === action.payload.id)[0];
53 | const index = state.surveys.indexOf(survey);
54 | if(index >= 0) {
55 | state.surveys.splice(index, 1);
56 | }
57 | })
58 | .addCase(update.fulfilled, (state, action) => {
59 | state.status = 'succeeded'
60 | // Update survey in the array
61 | const survey = state.surveys.filter(s => s.id === action.payload.id)[0];
62 | survey.json = action.payload.json;
63 | })
64 | }
65 | })
66 |
67 | export const load = createAsyncThunk('surveys/load', async () => {
68 | const response = await axios.get(apiBaseAddress + '/getActive')
69 | return response.data
70 | })
71 |
72 | export const get = createAsyncThunk('surveys/get', async (id: string) => {
73 | const response = await axios.get(apiBaseAddress + '/getSurvey?surveyId=' + id)
74 | return response.data
75 | })
76 |
77 | export const create = createAsyncThunk('surveys/create', async () => {
78 | const response = await axios.get(apiBaseAddress + '/create')
79 | return response.data
80 | })
81 |
82 | export const remove = createAsyncThunk('surveys/delete', async (id: string) => {
83 | const response = await axios.get(apiBaseAddress + '/delete?id=' + id)
84 | return response.data
85 | })
86 |
87 | export const update = createAsyncThunk('surveys/update', async (data: {id: string, json: any, text: string}) => {
88 | const response = await axios.post(apiBaseAddress + '/changeJson', data)
89 | return response.data
90 | })
91 |
92 | // export const { add, remove, update } = surveysSlice.actions
93 | export default surveysSlice.reducer
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, NavLink, Routes } from 'react-router-dom'
3 | import Home from "../pages/Home"
4 | import About from "../pages/About"
5 | import Run from "../pages/Run"
6 | import Edit from "../pages/Edit"
7 | import Results from "../pages/Results"
8 |
9 | export const NavBar = () => (
10 | <>
11 | My Surveys
12 | About
13 | >
14 | )
15 |
16 | const NoMatch = () => (<>404
>)
17 |
18 | const Content = (): React.ReactElement => (
19 | <>
20 |
21 | }>
22 | }>
23 | }>
24 | }>
25 | }>
26 | }>
27 |
28 | >
29 | )
30 |
31 | export default Content
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
7 | import { server } from './mocks/server'
8 | // Establish API mocking before all tests.
9 | beforeAll(() => server.listen())
10 | // Reset any request handlers that we may add during the tests,
11 | // so they don't affect other tests.
12 | afterEach(() => server.resetHandlers())
13 | // Clean up after the tests are finished.
14 | afterAll(() => server.close())
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------