├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── assets
│ └── img
│ │ ├── bermuda-welcome.png
│ │ ├── flame-sign-up.png
│ │ ├── mirage-list-is-empty.png
│ │ ├── task-done.png
│ │ └── user.jpg
├── components
│ ├── AnswerItem
│ │ ├── index.js
│ │ └── styles.js
│ ├── Avatar
│ │ ├── index.js
│ │ └── styles.js
│ ├── BackLink
│ │ ├── index.js
│ │ └── styles.js
│ ├── BaseContainer
│ │ ├── index.js
│ │ └── styles.js
│ ├── Button
│ │ ├── index.js
│ │ └── styles.js
│ ├── Card
│ │ ├── index.js
│ │ └── styles.js
│ ├── Header
│ │ ├── index.js
│ │ └── styles.js
│ ├── Input
│ │ ├── index.css
│ │ ├── index.js
│ │ └── styles.js
│ ├── ProtectedRoutes
│ │ └── adminProtected.js
│ ├── SizedBox
│ │ ├── index.js
│ │ └── styles.js
│ ├── SurveyCard
│ │ ├── index.js
│ │ └── styles.js
│ └── VectorContainer
│ │ ├── index.js
│ │ └── styles.js
├── contexts
│ └── auth.js
├── index.css
├── index.js
├── pages
│ ├── CreateSurvey
│ │ ├── index.js
│ │ └── styles.js
│ ├── Results
│ │ ├── index.js
│ │ └── styles.js
│ ├── SignIn
│ │ ├── SignIn.js
│ │ └── styles.js
│ ├── SignUp
│ │ ├── SignUp.js
│ │ └── styles.js
│ ├── Survey
│ │ ├── index.js
│ │ └── styles.js
│ └── Surveys
│ │ ├── index.js
│ │ └── styles.js
├── serviceWorker.js
├── services
│ └── api.js
├── setupTests.js
├── themes
│ └── index.js
└── utils
│ └── constants.js
└── yarn.lock
/.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 | # Survey App
2 |
3 | ## Live Demo
4 |
5 | [Survey Web App](https://chamatt.github.io/survey-web-app/)
6 |
7 | Admin Info (needed in case you want to create new surveys):
8 | - Email: admin@admin.com
9 | - Password: 123456
10 |
11 | *PS: The API is offline due to the fact that the mongodb addon for heroku got shutdown, but if you want, you can host your own api for this api.
12 | Backend API: https://github.com/chamatt/survey-api*
13 |
14 | ## Credits
15 |
16 | UI/UX Inpirations:
17 | - [uixNinja](https://dribbble.com/shots/8024190-Mobile-Application-Design)
18 | - [Bobby Saban](https://dribbble.com/shots/5440714-Concept-Survey-Builder)
19 |
20 | Front-end with React
21 | - [@chamatt](https://github.com/chamatt)
22 | - [@rezendegc](https://github.com/rezendegc)
23 |
24 | Back-end API Development:
25 | - [@brenoscalzer](https://github.com/brenoscalzer)
26 | - [@rezendegc](https://github.com/rezendegc)
27 |
28 | Vector Arts:
29 | - [Icons8](https://icons8.com.br/ouch)
30 |
31 | ## Screenshots
32 |
33 | |Homepage|Sign In|Create Survey|Survey|Results|
34 | |--------|-------|-------------|------|-------|
35 | ||||||
36 |
37 |
38 | ## Specifications
39 |
40 | This project was made following [app-ideas](https://github.com/florinpop17/app-ideas) Survey App specifications, in the tier 3 advanced category.
41 |
42 | **Tier:** 3-Advanced
43 |
44 | Surveys are a valuable part of any developers toolbox. They are useful for
45 | getting feedback from your users on a variety of topics including application
46 | satisfaction, requirements, upcoming needs, issues, priorities, and just plain
47 | aggravations to name a few.
48 |
49 | The Survey app gives you the opportunity to learn by developing a full-featured
50 | app that will you can add to your toolbox. It provides the ability to define a
51 | survey, allow users to respond within a predefined timeframe, and tabulate
52 | and present results.
53 |
54 | Users of this app are divided into two distinct roles, each having different
55 | requirements:
56 |
57 | - _Survey Coordinators_ define and conduct surveys. This is an administrative
58 | function not available to normal users.
59 | - _Survey Respondents_ Complete surveys and view results. They have no
60 | administrative privileges within the app.
61 |
62 | Commercial survey tools include distribution functionality that mass emails
63 | surveys to Survey Respondents. For simplicity, this app assumes that surveys
64 | open for responses will be accessed from the app's web page.
65 |
66 | ## User Stories
67 |
68 | ### General
69 |
70 | - [x] Survey Coordinators and Survey Respondents can define, conduct, and
71 | view surveys and survey results from a common website
72 | - [x] Survey Coordinators can login to the app to access administrative
73 | functions, like defining a survey.
74 |
75 | ### Defining a Survey
76 |
77 | - [x] Survey Coordinator can define a survey containing 1-10 multiple choice
78 | questions.
79 | - [x] Survey Coordinator can define 1-5 mutually exclusive selections to each
80 | question.
81 | - [x] Survey Coordinator can enter a title for the survey.
82 | - [x] Survey Coordinator can click a 'Cancel' button to return to the home
83 | page without saving the survey.
84 | - [x] Survey Coordinator can click a 'Save' button save a survey.
85 |
86 | ### Conducting a Survey
87 |
88 | - [x] Survey Coordinator can open a survey by selecting a survey from a
89 | list of previously defined surveys
90 | - [x] Survey Coordinators can close a survey by selecting it from a list of
91 | open surveys
92 | - [x] Survey Respondent can complete a survey by selecting it from a list of
93 | open surveys
94 | - [x] Survey Respondent can select responses to survey questions by clicking
95 | on a checkbox
96 | - [x] Survey Respondents can see that a previously selected response will
97 | automatically be unchecked if a different response is clicked.
98 | - [x] Survey Respondents can click a 'Cancel' button to return to the home
99 | page without submitting the survey.
100 | - [x] Survey Respondents can click a 'Submit' button submit their responses
101 | to the survey.
102 | - [x] Survey Respondents can see an error message if 'Submit' is clicked,
103 | but not all questions have been responded to.
104 |
105 | ### Viewing Survey Results
106 |
107 | - [x] Survey Coordinators and Survey Respondents can select the survey to
108 | display from a list of closed surveys
109 | - [x] Survey Coordinators and Survey Respondents can view survey results as
110 | in tabular format showing the number of responses for each of the possible
111 | selections to the questions.
112 |
113 | ## Bonus features
114 |
115 | - [x] Survey Respondents can create a unique account in the app
116 | - [x] Survey Respondents can login to the app
117 | - [x] Survey Respondents cannot complete the same survey more than once
118 | - [x] Survey Coordinators and Survey Respondents can view graphical
119 | representations of survey results (e.g. pie, bar, column, etc. charts)
120 |
121 | ## Useful links and resources
122 |
123 | Libraries for building surveys:
124 |
125 | - [SurveyJS](https://surveyjs.io/Overview/Library/)
126 |
127 | Some commercial survey services include:
128 |
129 | - [Survey Monkey](https://www.surveymonkey.com/)
130 | - [Traversy](https://youtu.be/SSDED3XKz-0)
131 | - [Typeform](https://www.typeform.com/)
132 |
133 |
134 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "survey-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "axios": "^0.19.2",
10 | "lodash": "^4.17.15",
11 | "materialize-css": "^1.0.0-rc.2",
12 | "react": "^16.12.0",
13 | "react-dom": "^16.12.0",
14 | "react-fetch-hook": "^1.7.1",
15 | "react-router-dom": "^5.1.2",
16 | "react-scripts": "3.3.1",
17 | "react-toastify": "^5.5.0",
18 | "styled-components": "^5.0.0",
19 | "use-state-with-callback": "^1.0.18"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build && cp build/index.html build/404.html",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject",
26 | "predeploy": "npm run build",
27 | "deploy": "gh-pages -d build"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | },
44 | "devDependencies": {
45 | "gh-pages": "^2.2.0"
46 | },
47 | "homepage": "http://chamatt.github.io/survey-web-app"
48 | }
49 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
17 |
21 |
22 |
26 |
30 |
34 |
43 | Survey App
44 |
45 |
46 |
47 |
48 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Survey App",
3 | "name": "Survey App",
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/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { ThemeProvider } from "styled-components";
3 | import { HashRouter as Router, Switch, Route } from "react-router-dom";
4 | import AuthContext from "./contexts/auth";
5 | import SignIn from "./pages/SignIn/SignIn";
6 | import themes from "./themes";
7 | import "materialize-css/dist/css/materialize.min.css";
8 | import Surveys from "./pages/Surveys";
9 | import SignUp from "./pages/SignUp/SignUp";
10 | import Results from "./pages/Results";
11 | import Survey from "./pages/Survey";
12 | import CreateSurvey from "./pages/CreateSurvey";
13 | import AdminRoute from "./components/ProtectedRoutes/adminProtected";
14 | import { ToastContainer } from "react-toastify";
15 | import "react-toastify/dist/ReactToastify.css";
16 | import "materialize-css";
17 | import { URL_ROOT, URL_CREATE, URL_REGISTER, URL_LOGIN, URL_SURVEY, URL_RESULTS } from "./utils/constants";
18 |
19 | function App() {
20 | const [user, setUser] = useState({ isLoggedIn: false });
21 | useEffect(() => {
22 | let userStorage = localStorage.getItem("user");
23 | if (userStorage) {
24 | userStorage = JSON.parse(userStorage);
25 | setUser(userStorage);
26 | }
27 | }, []);
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/src/assets/img/bermuda-welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/src/assets/img/bermuda-welcome.png
--------------------------------------------------------------------------------
/src/assets/img/flame-sign-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/src/assets/img/flame-sign-up.png
--------------------------------------------------------------------------------
/src/assets/img/mirage-list-is-empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/src/assets/img/mirage-list-is-empty.png
--------------------------------------------------------------------------------
/src/assets/img/task-done.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/src/assets/img/task-done.png
--------------------------------------------------------------------------------
/src/assets/img/user.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plutonurmux/survey_web_app/3b5ccd1ec829a988bf26651650bef9d803c0738e/src/assets/img/user.jpg
--------------------------------------------------------------------------------
/src/components/AnswerItem/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {
4 | Container,
5 | AnswerText,
6 | AnswerTextContainer,
7 | I,
8 | ResultPercentage
9 | } from "./styles";
10 |
11 | export default function AnswerItem({
12 | text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
13 | answerId,
14 | questionId,
15 | selected = false,
16 | selections,
17 | onSelect = () => {},
18 | id,
19 | showResults = false,
20 | resultPercent
21 | }) {
22 | const handleSelect = () => {
23 | onSelect({
24 | ...selections,
25 | [questionId]: answerId
26 | });
27 | };
28 |
29 | return (
30 |
35 |
36 | {text}
37 |
38 | {showResults ? (
39 | {`${parseInt(
40 | resultPercent * 100
41 | )}%`}
42 | ) : (
43 |
44 | {selected ? "check_box" : "check_box_outline_blank"}
45 |
46 | )}
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/AnswerItem/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | flex-direction: row;
6 | justify-content: space-between;
7 | align-items: center;
8 | cursor: pointer;
9 | border: 1px solid #ffffff0f;
10 | transition: all 0.1s;
11 |
12 | &:hover {
13 | background-color: rgba(100, 100, 110, 0.1);
14 | i {
15 | color: ${props => props.theme.colors.primary};
16 | }
17 | }
18 |
19 | ${props =>
20 | props.selected &&
21 | `
22 | background-color: ${props.theme.colors.secondary} !important;
23 | color: ${props.theme.colors.textNormal};
24 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
25 | `}
26 |
27 | ${props =>
28 | props.percentage &&
29 | `background-image: linear-gradient(90deg, ${
30 | props.theme.colors.secondary
31 | }44 ${parseInt(props.percentage * 100 - -1)}%, transparent ${parseInt(
32 | props.percentage * 100
33 | )}%, transparent ${parseInt(101 - props.percentage * 100)}%);
34 | border-right: none;
35 | `}
36 | `;
37 |
38 | export const AnswerTextContainer = styled.div`
39 | flex: 1;
40 | padding: 15px 5px;
41 | padding-left: 15px;
42 | `;
43 |
44 | export const AnswerText = styled.p`
45 | font-size: 1.3rem;
46 | user-select: none;
47 |
48 | @media (max-width: 900px) {
49 | font-size: 1.1rem;
50 | }
51 |
52 | ${props =>
53 | props.selected &&
54 | `
55 | color: ${props.theme.colors.textNormal};
56 | `}
57 | `;
58 |
59 | export const I = styled.i`
60 | user-select: none;
61 | padding: 20px;
62 | font-size: 2rem;
63 | ${props => props.selected && `color: ${props.theme.colors.primary};`}
64 | `;
65 |
66 | export const ResultPercentage = styled.div`
67 | padding-right: 10px;
68 | `;
69 |
--------------------------------------------------------------------------------
/src/components/Avatar/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Container, Img } from "./styles";
4 |
5 | export default function Avatar({ src, size, ...rest }) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Avatar/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | border-radius: 50%;
5 | width: ${props => props.size};
6 | height: ${props => props.size};
7 | overflow: hidden;
8 | `;
9 |
10 | export const Img = styled.img`
11 | height: 100%;
12 | width: 100%;
13 | object-fit: cover;
14 | `;
15 |
--------------------------------------------------------------------------------
/src/components/BackLink/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Container, I, BackText } from "./styles";
4 |
5 | export default function BackLink({ icon, onClick, children }) {
6 | return (
7 |
8 | chevron_left
9 | {children}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/BackLink/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import themes from "../../themes";
3 |
4 | export const Container = styled.div`
5 | display: flex;
6 | flex-direction: row;
7 | align-items: center;
8 | cursor: pointer;
9 | `;
10 |
11 | export const BackText = styled.p`
12 | color: ${props => themes.colors.secondary};
13 | `;
14 |
15 | export const I = styled.i`
16 | color: ${props => props.theme.colors.secondary};
17 | `;
18 |
--------------------------------------------------------------------------------
/src/components/BaseContainer/index.js:
--------------------------------------------------------------------------------
1 | import { Container } from "./styles";
2 |
3 | export default Container;
4 |
--------------------------------------------------------------------------------
/src/components/BaseContainer/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | max-width: 1200px;
5 | align-self: center;
6 | justify-self: center;
7 | flex: 1;
8 | `;
9 |
--------------------------------------------------------------------------------
/src/components/Button/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Btn } from "./styles";
4 |
5 | export default function Button({
6 | leftIcon,
7 | rightIcon,
8 | children,
9 | large,
10 | block,
11 | disabled,
12 | className,
13 | ...rest
14 | }) {
15 | return (
16 |
22 | {leftIcon && {leftIcon}}
23 | {children}
24 | {rightIcon && {rightIcon}}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/Button/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Btn = styled.button`
4 | background-color: ${props =>
5 | props.color ? props.theme.colors[props.color] : props.theme.colors.primary};
6 | color: ${props =>
7 | props.textColor ? props.textColor : props.theme.colors.secondary};
8 | border-radius: ${props => (props.rounded ? "50px" : "2px")};
9 |
10 | &:hover,
11 | &:focus,
12 | &:active {
13 | background-color: ${props =>
14 | props.color
15 | ? props.theme.colors[props.color]
16 | : props.theme.colors.primary} !important;
17 | filter: brightness(0.9);
18 | }
19 |
20 | ${props =>
21 | props.block &&
22 | `
23 | width: 100%;
24 | `}
25 | `;
26 |
--------------------------------------------------------------------------------
/src/components/Card/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Container } from "./styles";
4 |
5 | export default function Card({ children, ...rest }) {
6 | return {children};
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/Card/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | background-color: rgba(200, 200, 200, 0.1);
5 | padding: 50px;
6 | margin: 20px;
7 | box-shadow: 0px 5px 10px rgba(200, 150, 150, 0.1);
8 | `;
9 |
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import {
4 | Container,
5 | LeftContainer,
6 | RightContainer,
7 | Center,
8 | Card,
9 | User,
10 | LogoutButton
11 | } from "./styles";
12 | import Button from "../Button";
13 | import Avatar from "../Avatar";
14 | import { withRouter } from "react-router-dom";
15 | import AuthContext from "../../contexts/auth";
16 | import axiosInstance from "../../services/api";
17 | import VectorContainer from "../../components/VectorContainer";
18 | import signin_vector from "../../assets/img/flame-sign-up.png";
19 | import user_profile from "../../assets/img/user.jpg";
20 | import { COORDINATOR, URL_ROOT, URL_LOGIN, URL_CREATE } from '../../utils/constants';
21 |
22 | function Header({
23 | history,
24 | createSurvey = true,
25 | showUser = true,
26 | showHome = false,
27 | leftButtons
28 | }) {
29 | const { user, setUser } = useContext(AuthContext);
30 | const isAdmin = user?.data?.role?.toUpperCase() === COORDINATOR;
31 | const logout = () => {
32 | localStorage.removeItem("user");
33 | setUser({ isLoggedIn: false });
34 | axiosInstance.defaults.headers.common["Authorization"] = undefined;
35 | };
36 |
37 | const userInfo = !user?.isLoggedIn ? (
38 | history.push(URL_LOGIN)}>
39 | Login
40 |
44 |
45 | ) : (
46 |
47 | Welcome, {user.data.name}!{" "}
48 | {" "}Logout
49 |
50 | );
51 |
52 | return (
53 |
54 |
55 | {showHome ? (
56 | history.push(URL_ROOT)}>Home
57 | ) : (
58 | createSurvey &&
59 | isAdmin && (
60 |
63 | )
64 | )}
65 | {leftButtons && leftButtons}
66 |
67 |
68 | history.push(URL_ROOT)}
73 | >
74 |
75 | {showUser && userInfo}
76 |
77 | );
78 | }
79 |
80 | export default withRouter(Header);
81 |
--------------------------------------------------------------------------------
/src/components/Header/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import TCard from "../Card";
3 |
4 | export const Container = styled.div`
5 | height: 80px;
6 | width: 100%;
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: space-between;
10 | align-items: center;
11 | padding: 0 30px;
12 | min-width: 100%;
13 | margin-top: 30px;
14 | `;
15 |
16 | export const LeftContainer = styled.div`
17 | display: flex;
18 | flex-direction: row;
19 | justify-content: space-between;
20 | align-items: center;
21 | `;
22 | export const RightContainer = styled.div`
23 | display: flex;
24 | flex-direction: row;
25 | justify-content: space-between;
26 | align-items: center;
27 | `;
28 | export const Center = styled.div`
29 | position: absolute;
30 | left: 50%;
31 | transform: translateX(-50%);
32 | `;
33 |
34 | export const Card = styled(TCard)`
35 | margin: 0;
36 | padding: 10px;
37 | display: flex;
38 | flex-direction: row;
39 | justify-content: center;
40 | align-items: center;
41 | cursor: pointer;
42 | user-select: none;
43 | `;
44 |
45 | export const User = styled.p`
46 | margin: 0;
47 | padding-right: 15px;
48 | padding-left: 15px;
49 | color: ${props => props.theme.colors.secondary};
50 | font-weight: bold;
51 | `;
52 |
53 | export const LogoutButton = styled.span`
54 | cursor: pointer;
55 | color: red;
56 | `;
57 |
--------------------------------------------------------------------------------
/src/components/Input/index.css:
--------------------------------------------------------------------------------
1 | .input-field .prefix.active {
2 | color: white !important;
3 | }
4 |
5 | .input-field label.active {
6 | color: white !important;
7 | }
8 |
9 | .input-field input.active {
10 | border-bottom-color: white !important;
11 | box-shadow: white !important ;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Input/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./index.css";
3 | import { I, TInput, Label } from "./styles";
4 |
5 | export default function Input({ icon, label, id, ...rest }) {
6 | return (
7 |
8 | {icon && {icon}}
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/Input/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const I = styled.i`
4 | color: ${props => props.theme.colors.secondary};
5 | `;
6 |
7 | export const TInput = styled.input`
8 | color: ${props => props.theme.colors.secondary};
9 | `;
10 |
11 | export const Label = styled.label`
12 | color: ${props => props.theme.colors.secondary};
13 | `;
14 |
--------------------------------------------------------------------------------
/src/components/ProtectedRoutes/adminProtected.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { Route, Redirect } from "react-router-dom";
3 | import AuthContext from "../../contexts/auth";
4 | import { COORDINATOR } from '../../utils/constants';
5 |
6 | const ProtectedRoute = ({ component: Component, ...rest }) => {
7 | const { user } = useContext(AuthContext);
8 | const isAdmin = user?.data?.role?.toUpperCase() === COORDINATOR;
9 |
10 | return (
11 |
14 | isAdmin === true ? :
15 | }
16 | />
17 | );
18 | };
19 |
20 | export default ProtectedRoute;
21 |
--------------------------------------------------------------------------------
/src/components/SizedBox/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Container } from "./styles";
4 |
5 | export default function SizedBox({ ...props }) {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/SizedBox/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | height: ${props => props.height};
5 | width: ${props => props.width};
6 | `;
7 |
--------------------------------------------------------------------------------
/src/components/SurveyCard/index.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import {
4 | SurveyContainer,
5 | Title,
6 | TitleContainer,
7 | Category,
8 | Heading,
9 | Body,
10 | Description,
11 | Footer
12 | } from "./styles";
13 | import Button from "../Button";
14 | import { withRouter } from "react-router-dom";
15 | import AuthContext from "../../contexts/auth";
16 | import axiosInstace from "../../services/api";
17 | import SizedBox from "../../components/SizedBox";
18 | import {
19 | COORDINATOR,
20 | IDLE,
21 | ACTIVE,
22 | URL_SURVEY,
23 | CLOSED,
24 | URL_SURVEYS,
25 | URL_RESULTS
26 | } from "../../utils/constants";
27 | import { toast } from "react-toastify";
28 |
29 | const SurveyCard = ({
30 | history,
31 | title = "IT Executive Compensation Study",
32 | numQuestions,
33 | status,
34 | refetchData = () => {},
35 | surveyId = "123"
36 | }) => {
37 | const { user } = useContext(AuthContext);
38 | const isIdle = status?.toUpperCase() === IDLE;
39 | const isActive = status?.toUpperCase() === ACTIVE;
40 | const isAdmin = user?.data?.role?.toUpperCase() === COORDINATOR;
41 | const changeSurveyStatus = status => {
42 | return axiosInstace
43 | .put(`${URL_SURVEYS}/status/` + surveyId, { status })
44 | .then(refetchData);
45 | };
46 |
47 | const handleChangeStatusToActive = () => {
48 | changeSurveyStatus(ACTIVE)
49 | .then(() => toast.success("Survey moved to active."))
50 | .catch(() => toast.error("Error moving survey to active."));
51 | };
52 | const handleChangeStatusToClosed = () => {
53 | changeSurveyStatus(CLOSED)
54 | .then(() => toast.success("Survey moved to closed."))
55 | .catch(() => toast.error("Error moving survey to closed."));
56 | };
57 |
58 | return (
59 |
60 |
61 |
62 |
63 | event_noteSurvey
64 |
65 | {title}
66 |
67 |
68 |
69 |
70 | {numQuestions} question{numQuestions !== 1 ? "s" : ""} ({0.25 * numQuestions} minute{numQuestions !== 4 ? "s" : ""})
71 |
72 |
73 |
104 |
105 | );
106 | };
107 |
108 | export default withRouter(SurveyCard);
109 |
--------------------------------------------------------------------------------
/src/components/SurveyCard/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const SurveyContainer = styled.div`
4 | background-color: ${props => props.theme.colors.secondary};
5 | box-shadow: 0px 5px 10px rgba(200, 150, 150, 0.1);
6 | margin-right: 15px;
7 | margin-left: 15px;
8 | padding: 40px;
9 | border-radius: 15px;
10 | margin-bottom: 30px;
11 |
12 | @media (min-width: 1025px) {
13 | width: calc(33% - 30px);
14 | flex-basis: calc(33% - 30px);
15 | }
16 | @media (max-width: 1024px) {
17 | width: calc(50% - 30px);
18 | flex-basis: calc(50% - 30px);
19 | }
20 | @media (max-width: 768px) {
21 | width: calc(100% - 30px);
22 | flex-basis: calc(100% - 30px);
23 | }
24 | `;
25 |
26 | export const Title = styled.p`
27 | font-weight: bold;
28 | color: ${props => props.theme.colors.textNormal};
29 | font-size: 1.3rem;
30 | margin: 0;
31 | `;
32 |
33 | export const ImgContainer = styled.div`
34 | height: 80px;
35 | max-width: 150px;
36 | margin-right: 20px;
37 | `;
38 |
39 | export const Img = styled.img`
40 | object-fit: contain;
41 | height: 100%;
42 | border-radius: 15px;
43 | `;
44 |
45 | export const Category = styled.p`
46 | color: ${props => props.theme.colors.textNormal};
47 | font-size: 1.1rem;
48 | margin: 0;
49 | `;
50 | export const Heading = styled.div`
51 | display: flex;
52 | justify-content: flex-start;
53 | align-items: center;
54 | flex-direction: row;
55 | overflow: hidden;
56 | `;
57 | export const TitleContainer = styled.div`
58 | display: flex;
59 | justify-content: center;
60 | align-items: flex-start;
61 | flex-direction: column;
62 | `;
63 |
64 | export const Body = styled.div``;
65 |
66 | export const Description = styled.p`
67 | font-size: 1rem;
68 | color: ${props => props.theme.colors.textNormal};
69 | `;
70 |
71 | export const Footer = styled.div`
72 | padding-top: 10px;
73 | display: flex;
74 | flex-direction: column;
75 | align-items: stretch;
76 | `;
77 |
--------------------------------------------------------------------------------
/src/components/VectorContainer/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Container, Img } from "./styles";
4 |
5 | export default function VectorContainer({
6 | size = "200px",
7 | clickable,
8 | ...rest
9 | }) {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/VectorContainer/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | export const Container = styled.div`
3 | object-fit: contain;
4 | width: ${props => (props.size ? props.size : "200px")};
5 | ${props => props.clickable && "cursor: pointer"}
6 | `;
7 |
8 | export const Img = styled.img`
9 | width: 100%;
10 | `;
11 |
--------------------------------------------------------------------------------
/src/contexts/auth.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | const AuthContext = React.createContext();
3 |
4 | export default AuthContext;
5 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .html,
2 | body {
3 | height: 100%;
4 | background-color: #3646aa;
5 | }
6 |
7 | #root {
8 | min-height: 100vh;
9 | flex-direction: column;
10 | background-color: #3646aa;
11 | box-sizing: border-box;
12 | display: flex;
13 | }
14 |
15 | body {
16 | margin: 0;
17 | font-family: Montserrat, -apple-system, BlinkMacSystemFont, "Segoe UI",
18 | "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
19 | "Helvetica Neue", sans-serif;
20 | -webkit-font-smoothing: antialiased;
21 | -moz-osx-font-smoothing: grayscale;
22 | min-height: 100vh;
23 | display: flex;
24 | flex-direction: column;
25 | align-items: stretch;
26 | }
27 |
28 | code {
29 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
30 | monospace;
31 | }
32 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/src/pages/CreateSurvey/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | import React, { useState, useEffect } from "react";
3 |
4 | import {
5 | Container,
6 | SideBar,
7 | SideBarItem,
8 | QuestionCard,
9 | QuestionItemContainer,
10 | QuestionTitle,
11 | QuestionAction,
12 | Card,
13 | Button,
14 | QuestionTitleInput,
15 | OptionInput,
16 | OptionInputContainer,
17 | I,
18 | QuestionTitleInputContainer,
19 | Body,
20 | OptionAction,
21 | SurveyName,
22 | SurveyInput
23 | } from "./styles";
24 | import SizedBox from "../../components/SizedBox";
25 | import Header from "../../components/Header";
26 | import { debounce, uniqBy } from "lodash";
27 | import api from "../../services/api";
28 | import { toast } from "react-toastify";
29 | import {
30 | URL_ROOT,
31 | URL_SURVEYS,
32 | ACTIVE,
33 | IDLE,
34 | CANCEL
35 | } from "../../utils/constants";
36 |
37 | const defaultValue = {
38 | title: "",
39 | options: [""]
40 | };
41 |
42 | export default function CreateSurvey({ history }) {
43 | const [questions, setQuestions] = useState([{ ...defaultValue }]);
44 | const [selectedQuestion, setSelectedQuestion] = useState(0);
45 |
46 | const [options, setOptions] = useState([]);
47 | const [questionTitle, setQuestionTitle] = useState("");
48 | const [surveyTitle, setSurveyTitle] = useState("");
49 |
50 | useEffect(() => {
51 | setOptions(questions[selectedQuestion]?.options || []);
52 | }, [selectedQuestion]);
53 |
54 | useEffect(() => {
55 | setQuestionTitle(questions[selectedQuestion]?.title || "");
56 | }, [selectedQuestion]);
57 |
58 | const createNewQuestion = () => {
59 | if (questions.length >= 10) {
60 | toast.error("You can only have up to 10 questions");
61 | return;
62 | }
63 | setQuestions([...questions, { ...defaultValue }]);
64 | };
65 |
66 | const handleSelected = i => {
67 | setSelectedQuestion(i);
68 | };
69 |
70 | const updateOptions = (index, newOptions) => {
71 | const newQuestions = [...questions];
72 | newQuestions[index].options = newOptions || options;
73 | setQuestions(newQuestions);
74 | };
75 |
76 | const updateQuestionTitle = (index, newTitle) => {
77 | const newQuestions = [...questions];
78 | newQuestions[index].title = newTitle || questionTitle;
79 | setQuestions(newQuestions);
80 | };
81 |
82 | const handleOptionsChange = (e, index) => {
83 | const { value } = e.target;
84 | const newOptions = [...options];
85 | newOptions[index] = value;
86 | setOptions(newOptions);
87 | };
88 |
89 | const handleQuestionTitleChange = e => {
90 | const { value } = e.target;
91 | setQuestionTitle(value);
92 | };
93 |
94 | const addNewOption = () => {
95 | if (options.length >= 5) {
96 | toast.error("You can only add a maximum of 5 options");
97 | return;
98 | }
99 | const newOptions = [...options, ""];
100 | setOptions(newOptions);
101 | };
102 | const deleteOption = index => {
103 | if (options.length === 1) {
104 | toast.error("You need at least one option");
105 | return;
106 | }
107 | const newOptions = [...options];
108 | newOptions.splice(index, 1);
109 | setOptions(newOptions);
110 | };
111 |
112 | const deleteQuestion = index => {
113 | if (questions.length === 1) {
114 | toast.error("You need at least one question");
115 | return;
116 | }
117 | const newQuestions = [...questions];
118 | newQuestions.splice(index, 1);
119 | setQuestions(newQuestions);
120 | setSelectedQuestion(0);
121 | };
122 |
123 | useEffect(() => {
124 | const doDebounce = debounce(
125 | () => updateOptions(selectedQuestion, options),
126 | 100
127 | );
128 | doDebounce();
129 | }, [options]);
130 |
131 | useEffect(() => {
132 | const doDebounce = debounce(
133 | () => updateQuestionTitle(selectedQuestion, questionTitle),
134 | 100
135 | );
136 | doDebounce();
137 | }, [questionTitle]);
138 |
139 | const saveSurvey = (status = IDLE) => {
140 | const requestBody = {
141 | title: surveyTitle,
142 | description: "",
143 | questions: questions.map(q => ({
144 | ...q,
145 | options: uniqBy(q.options, a => a)
146 | })),
147 | status
148 | };
149 |
150 | if (questions.some(q => !q.title)) {
151 | toast.error("Error: Some question(s) are untitled");
152 | return;
153 | }
154 |
155 | if (questions.some(q => q.options.some(o => !o))) {
156 | toast.error("Error: Some options(s) are empty!");
157 | return;
158 | }
159 |
160 | api
161 | .post("/surveys", requestBody)
162 | .then(() => {
163 | toast.success("☑ Survey created successfuly!");
164 | history.push(URL_ROOT);
165 | })
166 | .catch(err => {
167 | toast.error(
168 | "Error creating survey: " + err?.response?.data?.message ||
169 | err?.response?.data
170 | );
171 | });
172 | };
173 |
174 | return (
175 |
176 | history.push(URL_SURVEYS)}
183 | >
184 | Cancel
185 | ,
186 | ,
193 |
200 | ]}
201 | />
202 |
203 |
204 | setSurveyTitle(e.target.value)}
208 | />
209 |
210 |
211 |
212 |
215 | {questions?.map((question, i) => (
216 | handleSelected(i)}
220 | >
221 |
222 | {question.title || "Untitled"}
223 |
224 |
225 | ))}
226 |
227 |
228 |
229 |
230 |
231 |
232 |
238 |
239 |
246 |
247 |
248 | {options.map((op, index) => {
249 | return (
250 |
251 | menu
252 | handleOptionsChange(e, index)}
256 | maxLength="250"
257 | >
258 |
259 | deleteOption(index)}>
260 | cancel
261 |
262 |
263 | );
264 | })}
265 |
268 |
269 |
270 |
271 |
272 | );
273 | }
274 |
--------------------------------------------------------------------------------
/src/pages/CreateSurvey/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import BaseContainer from "../../components/BaseContainer";
3 | import TCard from "../../components/Card";
4 | import TButton from "../../components/Button";
5 | import Input from "../../components/Input";
6 |
7 | export const Container = styled(BaseContainer)`
8 | display: flex;
9 | justify-content: flex-start;
10 | align-items: center;
11 | flex: 1;
12 | flex-direction: column;
13 | background-color: ${props => props.theme.colors.backgroundColor};
14 | max-width: 1200px;
15 | width: 100%;
16 | `;
17 |
18 | export const Body = styled.div`
19 | display: flex;
20 | justify-content: center;
21 | flex-direction: row;
22 | width: 100%;
23 | margin-top: 50px;
24 | `;
25 |
26 | export const SideBar = styled.div`
27 | display: flex;
28 | flex-direction: column;
29 | justify-content: flex-start;
30 | min-height: 600px;
31 | flex: 1;
32 | margin-left: 15px;
33 | margin-right: 15px;
34 |
35 | text-overflow: ellipsis;
36 | `;
37 | export const SideBarItem = styled.div`
38 | display: flex;
39 | flex-direction: row;
40 | align-items: center;
41 | justify-content: space-between;
42 | border-left: ${props => props.theme.colors.secondary};
43 | &:hover {
44 | background-color: ${props => `${props.theme.colors.secondary}0f`};
45 | cursor: pointer;
46 | }
47 |
48 | ${props =>
49 | props.selected
50 | ? `border-left: 4px solid ${props.theme.colors.secondary};
51 | color: ${props.theme.colors.secondary};
52 | font-weight: bold;`
53 | : `border-left: 4px solid ${props.theme.colors.secondary}44;
54 | color: ${props.theme.colors.secondary}44;
55 | font-weight: normal;`}
56 | `;
57 | export const QuestionItemContainer = styled.div`
58 | display: flex;
59 | flex-direction: column;
60 | align-items: flex-start;
61 | padding: 20px;
62 | `;
63 | export const QuestionNumber = styled.p`
64 | font-size: 1.3em;
65 | margin: 0;
66 | `;
67 | export const QuestionTitle = styled.p`
68 | font-size: 1.3em;
69 | margin: 0;
70 | `;
71 | export const QuestionAction = styled.div``;
72 |
73 | export const QuestionCard = styled.div`
74 | display: flex;
75 | flex: 4;
76 | min-height: 600px;
77 | `;
78 |
79 | export const Card = styled(TCard)`
80 | width: 100%;
81 | `;
82 |
83 | export const Button = styled(TButton)`
84 | margin: 15px !important;
85 | width: auto;
86 | `;
87 |
88 | export const QuestionTitleInput = styled(Input)`
89 | font-size: 30px !important;
90 | line-height: 30px !important;
91 | height: 60px !important;
92 | display: flex;
93 | flex: 1;
94 |
95 | & input {
96 | font-size: 30px;
97 | line-height: 30px;
98 | }
99 | & + label {
100 | font-size: 30px;
101 | line-height: 10px;
102 | pointer-events: none;
103 | padding-bottom: 5px;
104 | }
105 | `;
106 |
107 | export const OptionInputContainer = styled.div`
108 | display: flex;
109 | flex-direction: row;
110 | align-items: center;
111 | `;
112 |
113 | export const OptionInput = styled.input`
114 | color: ${props => props.theme.colors.secondary};
115 |
116 | background-color: transparent;
117 | border: none !important;
118 | box-shadow: none !important;
119 | margin-bottom: 0 !important;
120 |
121 | &:focus {
122 | border: none !important;
123 | box-shadow: none !important;
124 | }
125 | `;
126 |
127 | export const I = styled.i`
128 | color: ${props => props.theme.colors.secondary} !important;
129 |
130 | ${props =>
131 | props.danger &&
132 | `
133 | color: ${props.theme.colors.danger} !important;
134 | `}
135 | `;
136 |
137 | export const QuestionTitleInputContainer = styled.div`
138 | display: flex;
139 | flex-direction: row;
140 | align-items: center;
141 | justify-content: space-between;
142 | `;
143 |
144 | export const OptionAction = styled.div`
145 | cursor: pointer;
146 | `;
147 |
148 | export const SurveyName = styled.div`
149 | margin-top: 30px;
150 | `;
151 |
152 | export const SurveyInput = styled(QuestionTitleInput)`
153 | border-bottom: none !important;
154 | margin-bottom: 0 !important;
155 | text-align: center;
156 | `;
157 |
--------------------------------------------------------------------------------
/src/pages/Results/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 |
3 | import { Container, Title, Description, Card, Question } from "./styles";
4 | import axiosInstance from "../../services/api";
5 |
6 | import Header from "../../components/Header";
7 |
8 | import AnswerItem from "../../components/AnswerItem";
9 |
10 | export default function Survey({ history, match }) {
11 | const [data, setData] = useState();
12 | const mount = data =>
13 | data?.questions?.map(question => {
14 | const total = question.result.reduce((acc, cur) => cur + acc, 0);
15 |
16 | return (
17 |
18 | {"Question: " + question.title}
19 | {question?.options?.map((option, i) => (
20 |
28 | ))}
29 |
30 | );
31 | });
32 |
33 | useEffect(() => {
34 | axiosInstance
35 | .get(`/surveys/${match.params.id}/result`)
36 | .then(response => {
37 | setData(response?.data);
38 | })
39 | .catch();
40 | }, [match.params.id]);
41 |
42 | return (
43 |
44 |
45 | {data && (
46 | <>
47 | {data.title}
48 | {data.description}
49 | {mount(data)}
50 | >
51 | )}
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/Results/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import BaseContainer from "../../components/BaseContainer";
3 | import TCard from "../../components/Card";
4 |
5 | export const Container = styled(BaseContainer)`
6 | display: flex;
7 | justify-content: flex-start;
8 | align-items: center;
9 | flex: 1;
10 | flex-direction: column;
11 | background-color: ${props => props.theme.colors.backgroundColor};
12 | max-width: 1200px;
13 | width: 100%;
14 | `;
15 |
16 | export const Title = styled.h2`
17 | color: ${props => props.theme.colors.secondary};
18 | `;
19 |
20 | export const Description = styled.p`
21 | color: white;
22 | `;
23 |
24 | export const Card = styled(TCard)`
25 | width: 90%;
26 | margin-left: 15px;
27 | margin-right: 15px;
28 | margin-bottom: 50px;
29 | padding-left: 20px;
30 | padding-right: 20px;
31 | color: ${props => props.theme.colors.secondary};
32 |
33 | @media (min-width: 900px) {
34 | padding-left: 60px;
35 | padding-right: 60px;
36 | }
37 | `;
38 |
39 | export const Question = styled.h2`
40 | font-weight: bold;
41 | padding-left: 20px;
42 | padding-right: 20px;
43 | font-size: 2.5em;
44 | `;
45 |
--------------------------------------------------------------------------------
/src/pages/SignIn/SignIn.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from "react";
2 | import { Container, Title, Buttons, Card, ErrorMessage } from "./styles";
3 | import Input from "../../components/Input";
4 | import Button from "../../components/Button";
5 | import SizedBox from "../../components/SizedBox";
6 | import VectorContainer from "../../components/VectorContainer";
7 | import signin_vector from "../../assets/img/flame-sign-up.png";
8 | import AuthContext from "../../contexts/auth";
9 | import axiosInstance from "../../services/api";
10 | import { URL_ROOT, URL_REGISTER } from "../../utils/constants";
11 |
12 | export default function SignIn({ history }) {
13 | const { setUser, user } = useContext(AuthContext);
14 | const [email, setEmail] = useState();
15 | const [password, setPassword] = useState();
16 | const [error, setError] = useState();
17 |
18 | const login = async (email, password) => {
19 | try {
20 | const response = await axiosInstance.post("/users/auth", {
21 | email,
22 | password
23 | });
24 |
25 | axiosInstance.defaults.headers.common = {
26 | Authorization: `Bearer ${response.data.token}`
27 | };
28 |
29 | const userData = {
30 | ...user,
31 | isLoggedIn: true,
32 | data: response.data.user,
33 | token: response.data.token
34 | };
35 |
36 | setUser(userData);
37 |
38 | localStorage.setItem("user", JSON.stringify(userData));
39 |
40 | history.push(URL_ROOT);
41 | } catch ({ response }) {
42 | setError(response?.data?.message || "Unexpected error");
43 | }
44 | };
45 |
46 | const handleKeyPress = event => {
47 | if (event.key === "Enter") {
48 | login(email, password);
49 | }
50 | };
51 |
52 | return (
53 |
54 |
55 | Sign In
56 |
57 |
79 | {error}
80 |
81 |
82 |
83 |
84 |
87 |
88 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/src/pages/SignIn/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import BaseContainer from "../../components/BaseContainer";
3 | import TCard from "../../components/Card";
4 |
5 | export const Container = styled(BaseContainer)`
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | flex: 1;
10 | flex-direction: column;
11 | background-color: ${props => props.theme.colors.backgroundColor};
12 | width: 100%;
13 | margin-top: 50px;
14 | margin-bottom: 50px;
15 | `;
16 |
17 | export const Title = styled.h2`
18 | color: ${props => props.theme.colors.secondary};
19 | `;
20 |
21 | export const Buttons = styled.div`
22 | display: flex;
23 | flex-direction: column;
24 | align-self: stretch;
25 | justify-content: center;
26 | `;
27 |
28 | export const Card = styled(TCard)`
29 | max-width: 500px;
30 | width: 90%;
31 | `;
32 |
33 | export const ErrorMessage = styled.p`
34 | color: red;
35 | `;
36 |
--------------------------------------------------------------------------------
/src/pages/SignUp/SignUp.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from "react";
2 |
3 | import {
4 | Container,
5 | Title,
6 | Buttons,
7 | Card,
8 | ErrorMessage
9 | } from "../SignIn/styles";
10 | import Input from "../../components/Input";
11 | import Button from "../../components/Button";
12 | import SizedBox from "../../components/SizedBox";
13 | import VectorContainer from "../../components/VectorContainer";
14 | import signin_vector from "../../assets/img/flame-sign-up.png";
15 | import BackLink from "../../components/BackLink";
16 | import axiosInstance from "../../services/api";
17 | import AuthContext from "../../contexts/auth";
18 | import { URL_ROOT, URL_LOGIN } from "../../utils/constants";
19 |
20 | export default function SignUp({ history }) {
21 | const { user, setUser } = useContext(AuthContext);
22 | const [email, setEmail] = useState();
23 | const [password, setPassword] = useState();
24 | const [username, setUsername] = useState();
25 | const [name, setName] = useState();
26 | const [error, setError] = useState();
27 |
28 | const register = async () => {
29 | if (!email || !password || !username || !name) {
30 | setError("Please, fill all data.");
31 | return;
32 | }
33 | try {
34 | const response = await axiosInstance.post("/users", {
35 | email,
36 | password,
37 | username,
38 | name
39 | });
40 |
41 | axiosInstance.defaults.headers.common = {
42 | Authorization: `Bearer ${response.data.token}`
43 | };
44 |
45 | const userData = {
46 | ...user,
47 | isLoggedIn: true,
48 | data: response.data.user,
49 | token: response.data.token
50 | };
51 |
52 | setUser(userData);
53 |
54 | localStorage.setItem("user", JSON.stringify(userData));
55 |
56 | history.push(URL_ROOT);
57 | } catch ({ response }) {
58 | setError(response?.data?.message || response?.data || "Unexpected error");
59 | }
60 | };
61 |
62 | const handleKeyPress = event => {
63 | if (event.key === "Enter") {
64 | register();
65 | }
66 | };
67 |
68 | return (
69 |
70 |
71 | Sign Up
72 |
73 | history.push(URL_LOGIN)}>
74 | Back to Login
75 |
76 |
122 | {error}
123 |
124 |
125 |
128 |
129 |
130 |
131 | );
132 | }
133 |
--------------------------------------------------------------------------------
/src/pages/SignUp/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
--------------------------------------------------------------------------------
/src/pages/Survey/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from "react";
2 | import { Route } from "react-router-dom";
3 | import { Container, Card, Question, Buttons, Head } from "./styles";
4 | import AnswerItem from "../../components/AnswerItem";
5 | import Button from "../../components/Button";
6 | import themes from "../../themes";
7 | import SizedBox from "../../components/SizedBox";
8 | import BackLink from "../../components/BackLink";
9 | import Header from "../../components/Header";
10 | import VectorContainer from "../../components/VectorContainer";
11 | import done_vector from "../../assets/img/task-done.png";
12 | import bermuda_welcome from "../../assets/img/bermuda-welcome.png";
13 | import api from "../../services/api";
14 | import AuthContext from "../../contexts/auth";
15 | import { toast } from "react-toastify";
16 | import { URL_ROOT, URL_SURVEYS, URL_SURVEY } from "../../utils/constants";
17 |
18 | export default function Survey({ history, match }) {
19 | const [selections, setSelections] = useState({});
20 |
21 | const [isLoading, setIsLoading] = useState(false);
22 | const [data, setData] = useState({});
23 | const { user } = useContext(AuthContext);
24 | const fetchData = () => {
25 | setIsLoading(true);
26 | api
27 | .get(`/surveys/${match.params.surveyId}`)
28 | .then(response => {
29 | setIsLoading(false);
30 | setData(response.data);
31 | })
32 | .catch(({ response }) => {
33 | setIsLoading(false);
34 | });
35 | };
36 | useEffect(fetchData, [user]);
37 |
38 | if (isLoading) {
39 | return ;
40 | }
41 |
42 | const { questions, ...survey } = data;
43 |
44 | const goToNextPage = () => {
45 | const nextPageId = getNextPage(getQuestionId(match.url), questions);
46 | if (nextPageId) {
47 | history.push(
48 | `${getUrlWithoutLastPart(match.url)}/${getNextPage(
49 | getQuestionId(match.url),
50 | questions
51 | )}`
52 | );
53 | } else {
54 | history.push(`${URL_SURVEY}/${match.params.surveyId}/complete`);
55 | }
56 | };
57 |
58 | const submitSurvey = () => {
59 | const requestBody = {
60 | survey: survey.id,
61 | answers: []
62 | };
63 |
64 | requestBody.answers = Object.entries(selections).map(
65 | ([question, answer]) => ({
66 | question,
67 | answer
68 | })
69 | );
70 |
71 | api
72 | .post("/entries", requestBody)
73 | .then(() => {
74 | toast.success("☑ Survey submitted successfuly!");
75 | history.push(URL_ROOT);
76 | })
77 | .catch(err => {
78 | toast.error("Error submiting survey: " + err?.response?.data?.message);
79 | });
80 | };
81 |
82 | return (
83 |
84 |
85 |
86 |
87 | history.goBack()}>Back to surveys
88 |
89 | Take a quick survey
90 |
91 |
92 |
93 |
94 | {questions?.length} QUESTION{questions?.length !== 1 ? "S" : ""}
95 |
96 |
97 | {" "}
98 | {0.25 * questions?.length} MINUTE
99 | {questions?.length !== 4 ? "S" : ""}
100 |
101 |
102 |
103 | {!isLoading && data.taken && "You already completed this survey"}
104 |
105 | {!isLoading && (
106 |
107 | {data.taken ? (
108 |
118 | ) : (
119 |
133 | )}
134 |
135 | )}
136 |
137 |
138 |
139 | {questions?.map((question, i) => {
140 | return (
141 |
146 |
147 | history.goBack()}>
148 | {i > 0 ? "Previous Question" : "Go back"}
149 |
150 | {question.title}
151 |
152 | {question.options.map(text => (
153 |
162 | ))}
163 |
164 |
175 |
176 |
177 |
178 | );
179 | })}
180 |
181 |
182 |
183 | history.goBack()}>Go back
184 |
185 |
186 | Survey Completed
187 |
188 |
189 |
190 |
191 |
192 |
200 |
201 |
210 |
211 |
212 |
213 |
214 | );
215 | }
216 |
217 | function getUrlWithoutLastPart(url) {
218 | return url
219 | .split("/")
220 | .slice(0, -1)
221 | .join("/");
222 | }
223 |
224 | function getNextPage(id, arr) {
225 | const currentPageNumber = arr.findIndex(el => el.id === id);
226 |
227 | const nextPage = arr[currentPageNumber + 1];
228 | if (nextPage?.id) return nextPage.id;
229 | return null;
230 | }
231 |
232 | function getQuestionId(url) {
233 | return url
234 | .split("/")
235 | .slice(-1)
236 | .join("");
237 | }
238 |
--------------------------------------------------------------------------------
/src/pages/Survey/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import BaseContainer from "../../components/BaseContainer";
3 | import TCard from "../../components/Card";
4 |
5 | export const Container = styled(BaseContainer)`
6 | display: flex;
7 | justify-content: flex-start;
8 | align-items: center;
9 | flex: 1;
10 | flex-direction: column;
11 | background-color: ${props => props.theme.colors.backgroundColor};
12 | max-width: 1200px;
13 | width: 100%;
14 | `;
15 |
16 | export const Card = styled(TCard)`
17 | width: 90%;
18 | margin-left: 15px;
19 | margin-right: 15px;
20 | margin-bottom: 50px;
21 | padding-left: 20px;
22 | padding-right: 20px;
23 | color: ${props => props.theme.colors.secondary};
24 |
25 | @media (min-width: 900px) {
26 | padding-left: 60px;
27 | padding-right: 60px;
28 | }
29 | `;
30 |
31 | export const Question = styled.h2`
32 | font-weight: bold;
33 | padding-left: 20px;
34 | padding-right: 20px;
35 | font-size: 2.5em;
36 | `;
37 | export const Buttons = styled.div`
38 | display: flex;
39 | flex-direction: row;
40 | align-items: center;
41 | justify-content: center;
42 | padding-top: 50px;
43 | `;
44 |
45 | export const Head = styled.div`
46 | display: flex;
47 | flex-direction: column;
48 | text-align: center;
49 | align-items: center;
50 | `;
51 |
--------------------------------------------------------------------------------
/src/pages/Surveys/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from "react";
2 |
3 | import {
4 | Container,
5 | SurveyGrid,
6 | Title,
7 | EmptyWarning,
8 | EmptyWarningText
9 | } from "./styles";
10 | import SurveyCard from "../../components/SurveyCard";
11 | import Header from "../../components/Header";
12 | import SizedBox from "../../components/SizedBox";
13 | import axiosInstace from "../../services/api";
14 | import AuthContext from "../../contexts/auth";
15 | import VectorContainer from "../../components/VectorContainer";
16 | import empty_list from "../../assets/img/mirage-list-is-empty.png";
17 | import { CLOSED, IDLE, ACTIVE } from '../../utils/constants';
18 |
19 | export default function Surveys() {
20 | const [loading, setLoading] = useState(true);
21 | const [closedSurveys, setClosedSurveys] = useState();
22 | const [openSurveys, setOpenSurveys] = useState();
23 | const [idleSurveys, setIdleSurveys] = useState();
24 | const { user } = useContext(AuthContext);
25 |
26 | const fetchData = () => {
27 | axiosInstace
28 | .get("/surveys/")
29 | .then(response => {
30 | setLoading(false);
31 | setClosedSurveys(response?.data?.filter(s => s.status === CLOSED));
32 | setOpenSurveys(response?.data?.filter(s => s.status === ACTIVE));
33 | setIdleSurveys(response?.data?.filter(s => s.status === IDLE));
34 | })
35 | .catch(({ response }) => {
36 | setLoading(false);
37 | });
38 | };
39 |
40 | useEffect(fetchData, [user]);
41 |
42 | return (
43 |
44 |
45 | <>
46 |
47 | Active Surveys
48 |
49 | {!openSurveys?.length && (
50 |
51 |
52 | No Active Surveys Available
53 |
54 | )}
55 |
56 | {!loading &&
57 | openSurveys?.length &&
58 | openSurveys?.map(survey => (
59 |
67 | ))}
68 |
69 | >
70 | {!loading && idleSurveys?.length > 0 && (
71 | <>
72 |
73 | Idle Surveys
74 |
75 |
76 | {idleSurveys?.map(survey => (
77 |
85 | ))}
86 |
87 | >
88 | )}
89 | <>
90 |
91 | Closed Surveys
92 |
93 | {!closedSurveys?.length && (
94 |
95 |
96 | No Closed Surveys Available
97 |
98 | )}
99 |
100 | {!loading &&
101 | closedSurveys?.length &&
102 | closedSurveys?.map(survey => (
103 |
111 | ))}
112 |
113 | >
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/src/pages/Surveys/styles.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Container = styled.div`
4 | display: flex;
5 | justify-content: flex-start;
6 | align-items: center;
7 | align-self: center;
8 | flex: 1;
9 | flex-direction: column;
10 | background-color: ${props => props.theme.colors.backgroundColor};
11 | max-width: 1200px;
12 | width: 100%;
13 | margin-bottom: 50px;
14 | `;
15 |
16 | export const SurveyGrid = styled.div`
17 | display: flex;
18 | justify-content: stretch;
19 | align-items: flex-start;
20 | flex-direction: row;
21 | flex-wrap: wrap;
22 | width: 100%;
23 | `;
24 |
25 | export const Title = styled.h2`
26 | color: ${props => props.theme.colors.secondary};
27 | `;
28 |
29 | export const EmptyWarning = styled.div`
30 | display: flex;
31 | flex-direction: column;
32 | align-items: center;
33 | `;
34 |
35 | export const EmptyWarningText = styled.h6`
36 | color: ${props => props.theme.colors.secondary};
37 | `;
38 |
--------------------------------------------------------------------------------
/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.0/8 are 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 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' }
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready.then(registration => {
134 | registration.unregister();
135 | });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/services/api.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 |
3 | const instance = axios.create({
4 | baseURL: "https://chamatt-survey-api.herokuapp.com/"
5 | });
6 | let userStorage = localStorage.getItem("user");
7 |
8 | if (userStorage) {
9 | userStorage = JSON.parse(userStorage);
10 | instance.defaults.headers.common = {
11 | Authorization: `Bearer ${userStorage.token}`
12 | };
13 | }
14 |
15 | export default instance;
16 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
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/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/themes/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | colors: {
3 | backgroundColor: "#3646AA",
4 | primary: "#5CD4A0",
5 | secondary: "#F2F2F2",
6 | danger: "#9B291F",
7 | purple: "#8e44ad",
8 | textNormal: "#3C4659",
9 | textLight: "#ADADC4",
10 | red: "#8F0000",
11 | green: "#3FA69A"
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const COORDINATOR = "COORDINATOR";
2 | export const IDLE = "IDLE";
3 | export const ACTIVE = "ACTIVE";
4 | export const CLOSED = "CLOSED";
5 | export const CANCEL = "CANCEL";
6 |
7 | // URL
8 | export const URL_ROOT = "/";
9 | export const URL_LOGIN = "/login";
10 | export const URL_CREATE = "/create";
11 | export const URL_REGISTER = "/register";
12 | export const URL_SURVEY = "/survey";
13 | export const URL_SURVEYS = "/surveys";
14 | export const URL_RESULTS = "/results";
15 |
--------------------------------------------------------------------------------