├── .github └── workflows │ └── codecov.yml ├── .gitignore ├── .idea ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── next.iml ├── vcs.xml └── watcherTasks.xml ├── LICENSE ├── README.md ├── __tests__ └── root.spec.tsx ├── components ├── Answers │ └── ScoreSlider.tsx ├── FormElements │ ├── Input.tsx │ ├── Select.tsx │ └── Slider.tsx ├── Layout │ ├── Layout.module.scss │ ├── Layout.tsx │ ├── SideNav.tsx │ └── TopNav.tsx ├── Navigation │ ├── Navigation.module.scss │ └── Navigation.tsx ├── PageTransition.tsx ├── Question │ ├── Chart.tsx │ ├── Question.tsx │ └── Results.tsx └── hoc │ └── QuestionnaireHOC.tsx ├── enzyme.js ├── hooks ├── useActions.ts └── useTypedSelector.ts ├── jest.config.js ├── jest.tsconfig.json ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── index.tsx ├── overview │ └── [disease].tsx └── question │ ├── [id].tsx │ └── results.tsx ├── public ├── DOCTOR.png ├── LIFE_SUPPORT.png ├── PNG │ ├── Drawkit-Vector-Illustration-Medical-01.png │ ├── Drawkit-Vector-Illustration-Medical-02.png │ ├── Drawkit-Vector-Illustration-Medical-03.png │ ├── Drawkit-Vector-Illustration-Medical-04.png │ ├── Drawkit-Vector-Illustration-Medical-05.png │ ├── Drawkit-Vector-Illustration-Medical-06.png │ ├── Drawkit-Vector-Illustration-Medical-07.png │ ├── Drawkit-Vector-Illustration-Medical-08.png │ ├── Drawkit-Vector-Illustration-Medical-09.png │ ├── Drawkit-Vector-Illustration-Medical-10.png │ ├── Drawkit-Vector-Illustration-Medical-11.png │ ├── Drawkit-Vector-Illustration-Medical-12.png │ ├── Drawkit-Vector-Illustration-Medical-13.png │ ├── Drawkit-Vector-Illustration-Medical-14.png │ ├── Drawkit-Vector-Illustration-Medical-15.png │ ├── Drawkit-Vector-Illustration-Medical-16.png │ ├── Drawkit-Vector-Illustration-Medical-17.png │ ├── Drawkit-Vector-Illustration-Medical-18.png │ ├── Drawkit-Vector-Illustration-Medical-19.png │ └── Drawkit-Vector-Illustration-Medical-20.png ├── Poster.png ├── SVG │ ├── Drawkit-Vector-Illustration-Medical-01.svg │ ├── Drawkit-Vector-Illustration-Medical-02.svg │ ├── Drawkit-Vector-Illustration-Medical-03.svg │ ├── Drawkit-Vector-Illustration-Medical-04.svg │ ├── Drawkit-Vector-Illustration-Medical-05.svg │ ├── Drawkit-Vector-Illustration-Medical-06.svg │ ├── Drawkit-Vector-Illustration-Medical-07.svg │ ├── Drawkit-Vector-Illustration-Medical-08.svg │ ├── Drawkit-Vector-Illustration-Medical-09.svg │ ├── Drawkit-Vector-Illustration-Medical-10.svg │ ├── Drawkit-Vector-Illustration-Medical-11.svg │ ├── Drawkit-Vector-Illustration-Medical-12.svg │ ├── Drawkit-Vector-Illustration-Medical-13.svg │ ├── Drawkit-Vector-Illustration-Medical-14.svg │ ├── Drawkit-Vector-Illustration-Medical-15.svg │ ├── Drawkit-Vector-Illustration-Medical-16.svg │ ├── Drawkit-Vector-Illustration-Medical-17.svg │ ├── Drawkit-Vector-Illustration-Medical-18.svg │ ├── Drawkit-Vector-Illustration-Medical-19.svg │ └── Drawkit-Vector-Illustration-Medical-20.svg ├── Vector.png ├── cardiovascular.png ├── diabetes.png └── stroke.png ├── redux ├── action-creators │ ├── index.ts │ └── questions.ts ├── actions │ └── questions.ts ├── api.ts ├── reducers │ ├── index.ts │ └── questions.ts ├── store.ts └── types │ ├── actions.ts │ └── questions.ts ├── router └── nav.ts ├── styles ├── Router.module.scss ├── globals.css └── theme │ └── theme.ts ├── tsconfig.json ├── utils └── risk.ts └── yarn.lock /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Running Code Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [14.x] 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 2 19 | 20 | - name: Set up Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Run tests 29 | run: npm run test:coverage 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v1 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/next.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Doctorinna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doctorinna-Frontend 2 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d6c01263e90b4c3a9e85b630e274eb72)](https://www.codacy.com/gh/Doctorinna/frontend/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Doctorinna/frontend&utm_campaign=Badge_Grade) 3 | [![codecov](https://codecov.io/gh/Doctorinna/frontend/branch/master/graph/badge.svg?token=8BE2XA4162)](https://codecov.io/gh/Doctorinna/frontend) 4 | 5 | This repository is place for Doctorinna frontend project. It is built using next js framework on react, with redux as state manager and material ui components. Project is written on typescript to allow code completion and catching bugs more easily. Current version is just layout of a SPA. 6 | 7 | ## Getting Started 8 | For project I use Node.js 14, but most of packages are working on different versions of node, if you have a problems with sass [check supported versions](https://github.com/sass/node-sass) and use different version of node/node-sass(edit package.json and package-lock.json) package 9 | 10 | To learn your version of node run: 11 | 12 | ```bash 13 | node --version 14 | ``` 15 | 16 | To install dependencies run: 17 | 18 | ```bash 19 | npm install 20 | # or 21 | yarn install 22 | ``` 23 | 24 | To run the development server: 25 | 26 | ```bash 27 | npm run dev 28 | # or 29 | yarn dev 30 | ``` 31 | 32 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 33 | 34 | ## MVC 35 | MVC of applicatiom is deployed to vercel platform that already has CI. 36 | 37 | But unfortunately, vercel only permit https, so it isn't integrated with backend yet. It works locally though. 38 | 39 | You can see the app here [https://doctorinna.vercel.app/](https://doctorinna.vercel.app/) 40 | -------------------------------------------------------------------------------- /__tests__/root.spec.tsx: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import React from "react"; 3 | import {ReactWrapper, shallow, mount, ShallowWrapper} from "enzyme"; 4 | import {Box, Grid} from "@mui/material"; 5 | import Index from "../pages/index"; 6 | import QuestionnaireHoc from "../components/hoc/QuestionnaireHOC"; 7 | import {Provider} from "react-redux"; 8 | import {makeStore} from "../redux/store"; 9 | import Layout from "../components/Layout/Layout"; 10 | 11 | describe("Pages", ()=>{ 12 | describe("Index page", ()=>{ 13 | it("All Grid components in root page are rendered", ()=>{ 14 | const wrapper: ReactWrapper = mount( 15 | 16 | ); 17 | const grid_number = wrapper.find(Grid).length; 18 | expect(grid_number).toEqual(16); 19 | }); 20 | it("Layout is rendered", ()=>{ 21 | const wrapper: ShallowWrapper = shallow(); 22 | const box_number = wrapper.find(Box).length; 23 | expect(box_number).toEqual(2); 24 | }) 25 | it("All Grid components in questionnaire HOC are rendered", ()=>{ 26 | const wrapper: ReactWrapper = mount(); 27 | const grid_number = wrapper.find(Grid).length; 28 | expect(grid_number).toEqual(11); 29 | }) 30 | }) 31 | }) -------------------------------------------------------------------------------- /components/Answers/ScoreSlider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Slider} from "@mui/material"; 3 | 4 | interface scoreSliderProps { 5 | value: number//between 0 and 1 6 | } 7 | const ScoreSlider: React.FC = ({value}) => { 8 | const valueText = (val: number) => { 9 | const rounded = Math.round(val*1000); 10 | return rounded/10 + "%"; 11 | } 12 | const N = 10; 13 | const marks = Array.from({length: N}, (v, k) => (k+1)*10).map(val => ({value: val, label:val})); 14 | return ( 15 | 25 | ); 26 | }; 27 | 28 | export default ScoreSlider; -------------------------------------------------------------------------------- /components/FormElements/Input.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {TextField} from "@mui/material"; 3 | 4 | interface InputProps { 5 | value: string, 6 | handleValue: ()=>void 7 | } 8 | 9 | const Input:React.FC = ({value, handleValue}) => { 10 | return ( 11 | 12 | ); 13 | }; 14 | 15 | export default Input; -------------------------------------------------------------------------------- /components/FormElements/Select.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | MenuItem, 4 | Select as Sel, 5 | FormControl, 6 | SelectChangeEvent, 7 | FormHelperText 8 | } from "@mui/material"; 9 | import {Option} from "../../redux/types/questions"; 10 | 11 | interface SelectProps { 12 | options: Option[], 13 | value: string, 14 | handleValue: (e: SelectChangeEvent)=>void; 15 | hasError: boolean; 16 | } 17 | 18 | const Select:React.FC = ({options, value, handleValue, hasError}) => { 19 | return ( 20 | 21 | 29 | {options.map((opt)=>( 30 | {opt.answer} 31 | ))} 32 | 33 | {hasError && This is required!} 34 | 35 | ); 36 | }; 37 | 38 | export default Select; -------------------------------------------------------------------------------- /components/FormElements/Slider.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import {Box, Checkbox, FormControlLabel, Slider as Slid} from "@mui/material"; 3 | import {Option} from "../../redux/types/questions"; 4 | 5 | interface sliderProps { 6 | max: number; 7 | min: number; 8 | value: number; 9 | handler: (e: Event, val: number | number[]) => void; 10 | options: Option[]; 11 | optionHandler: (checked: boolean) => void 12 | } 13 | 14 | const Slider: React.FC = ({optionHandler, max, min, value, handler, options}) => { 15 | const [dis, setDis] = useState(false); 16 | const N = 5; 17 | const marks = Array.from({length: N + 1}, (v, k) => Math.round(min + (max - min) * k / N)).map(val => ({ 18 | value: val, 19 | label: val.toString() 20 | })); 21 | return ( 22 | 23 | 46 | {options[0] ? 47 | ) => { 50 | setDis(e.target.checked); 51 | optionHandler(e.target.checked); 52 | }} 53 | inputProps={{"aria-label": "controlled"}}/>} label={options[0].answer}/> 54 | : null} 55 | 56 | ); 57 | }; 58 | 59 | export default Slider; -------------------------------------------------------------------------------- /components/Layout/Layout.module.scss: -------------------------------------------------------------------------------- 1 | .Container { 2 | min-width: 100vw; 3 | min-height: 100vh; 4 | background-color: #E5E5E5; 5 | justify-content: center; 6 | align-items:center; 7 | padding: 0.25vh 0.25vw; 8 | } 9 | 10 | .Box { 11 | margin: 1vh 1vw; 12 | background: white; 13 | justify-content: center; 14 | box-shadow: 0.25vh 0.25vh 0.5vh dimgray; 15 | } -------------------------------------------------------------------------------- /components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Box} from "@mui/material"; 3 | import classes from "./Layout.module.scss"; 4 | 5 | const Layout: React.FC = (props) => { 6 | return ( 7 | 8 | 9 | {props.children} 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default Layout; -------------------------------------------------------------------------------- /components/Layout/SideNav.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Box, Grid, Typography} from "@mui/material"; 3 | 4 | interface SideNavProps { 5 | underline: number 6 | } 7 | const SideNav:React.FC = ({underline}) => { 8 | const links = ["Start", "Questionnaire", "Results", "Overview"]; 9 | return ( 10 | 11 | 12 | 13 | 14 | heart 15 | 16 | 17 | Doctorinna 18 | 19 | 20 | 21 | 22 | {links.map((v,ind) => ( 23 | 24 | {v} 28 | 29 | ))} 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default SideNav; -------------------------------------------------------------------------------- /components/Layout/TopNav.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Box, Grid, Typography} from "@mui/material"; 3 | import {useTypedSelector} from "../../hooks/useTypedSelector"; 4 | 5 | interface TopNav { 6 | chosen: string 7 | } 8 | 9 | const TopNav: React.FC = ({chosen}) => { 10 | const {categories} = useTypedSelector(state => state.questions); 11 | return ( 12 | 15 | 16 | {categories.map((val, ind) => ( 17 | 18 | 19 | {val.title} 21 | 22 | 23 | {ind !== categories.length - 1 ? 24 | . 25 | : null 26 | } 27 | 28 | 29 | ))} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default TopNav; -------------------------------------------------------------------------------- /components/Navigation/Navigation.module.scss: -------------------------------------------------------------------------------- 1 | .AppBar { 2 | height: 5vh; 3 | min-height: 50px; 4 | } 5 | -------------------------------------------------------------------------------- /components/Navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {AppBar, Toolbar, IconButton, Typography, Grid} from "@mui/material"; 3 | import {Menu as MenuIcon} from "@mui/icons-material"; 4 | import {nav} from "../../router/nav"; 5 | import classes from "./Navigation.module.scss"; 6 | import Link from "next/link"; 7 | 8 | const Navigation: React.FC = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {nav.map((el, ind) => ( 23 | 24 | 25 | 26 | {el.name} 27 | 28 | 29 | 30 | 31 | 32 | ))} 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default Navigation; -------------------------------------------------------------------------------- /components/PageTransition.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Transition, TransitionGroup} from "react-transition-group"; 3 | 4 | interface TransitionProps { 5 | children: T, 6 | location: string 7 | } 8 | 9 | const TIMEOUT: number = 450; 10 | const TIMING_FUNCTION : string = "cubic-bezier(0,1.5,1,1.05)"; 11 | 12 | const getTransitionStyles:any = { 13 | entering: { 14 | position: "absolute", 15 | opacity: 0, 16 | transform: "translateX(-50%)", 17 | }, 18 | entered: { 19 | transition: `opacity ${TIMEOUT}ms ${TIMING_FUNCTION}, transform ${TIMEOUT}ms ${TIMING_FUNCTION}`, 20 | opacity: 1, 21 | transform: "translateX(0px)", 22 | 23 | }, 24 | exiting: { 25 | transition: `opacity ${TIMEOUT}ms ${TIMING_FUNCTION}, transform ${TIMEOUT}ms ${TIMING_FUNCTION}`, 26 | opacity: 0, 27 | transform: "translateX(50%)", 28 | }, 29 | }; 30 | 31 | const PageTransition:React.FC> = ({location, children}) => { 32 | return ( 33 | 34 | 35 | {(state) => ( 36 |
{children}
37 | )} 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default PageTransition; -------------------------------------------------------------------------------- /components/Question/Chart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Box, Typography} from "@mui/material"; 3 | import {Statistics} from "../../redux/types/questions"; 4 | import {parseRisk} from "../../utils/risk"; 5 | import {purple} from "@mui/material/colors"; 6 | 7 | 8 | interface chartProps { 9 | stats: Statistics 10 | } 11 | 12 | const Chart: React.FC = ({stats}) => { 13 | const colors = [purple["900"], purple["700"], purple["500"], 14 | purple["300"], purple["100"], purple["50"]]; 15 | let max = stats.country[0]?.avg_factor; 16 | stats.country.forEach((r) => { 17 | if (r.avg_factor > max) max = r.avg_factor; 18 | }); 19 | const scale = (percent: number) => percent / parseRisk(max) * 100; 20 | 21 | return (<> 22 | {stats.country.map((r, index) => ( 23 | 24 | 30 | {r.region} 31 | 32 | 33 | ))} 34 | 35 | ); 36 | }; 37 | 38 | export default Chart; -------------------------------------------------------------------------------- /components/Question/Question.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import { 3 | SelectChangeEvent, 4 | Box 5 | } from "@mui/material"; 6 | import Select from "../FormElements/Select"; 7 | import Slider from "../FormElements/Slider"; 8 | import {AnswerType, QuestionType} from "../../redux/types/questions"; 9 | 10 | interface QuestionProps { 11 | question: QuestionType, 12 | answer: AnswerType, 13 | setAns: (answer: AnswerType) => void, 14 | hasError: boolean 15 | } 16 | 17 | const Question: React.FC = ({question, answer, setAns, hasError}) => { 18 | let elementType: JSX.Element; 19 | if (question.range) { 20 | const max = question.range.max; 21 | const min = question.range.min; 22 | setAns({...answer, answer: Math.round((min+max)/2).toString()}); 23 | const [value, setValue] = useState(Math.round((min+max)/2)); 24 | let oldValue : number; 25 | elementType = { 30 | setValue(val as number); 31 | setAns({...answer, answer: (val as number).toString()}) 32 | } 33 | } 34 | optionHandler={ 35 | (checked: boolean) => { 36 | if(checked){ 37 | oldValue = value; 38 | setAns({...answer, answer:question.options[0].answer}); 39 | } 40 | else setAns({...answer, answer:oldValue.toString()}); 41 | } 42 | } 43 | options={question.options}/> 44 | } 45 | else { 46 | const [value, setValue] = useState(question.options[0].answer); 47 | setAns({...answer, answer: question.options[0].answer}); 48 | elementType =