-
-
-
-

-
+ return (
+ <>
+
+
+
+
+

+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
Loading API Fetch: {isLoading.toString()}
- {response &&
-
API Fetch Result: {JSON.stringify(response)}
- }
- >
- )
+
Loading API Fetch: {isLoading.toString()}
+ {response &&
API Fetch Result: {JSON.stringify(response)}
}
+ >
+ )
}
diff --git a/pwa/src/components/PatientInfoForm.test.tsx b/pwa/src/components/PatientInfoForm.test.tsx
new file mode 100644
index 0000000..0311782
--- /dev/null
+++ b/pwa/src/components/PatientInfoForm.test.tsx
@@ -0,0 +1,147 @@
+import React from 'react'
+import { render, fireEvent, prettyDOM } from '@testing-library/react'
+import PatientInfoForm from './PatientInfoForm'
+import { StoreProvider } from 'easy-peasy'
+import store from '../state/store'
+
+test('female-only div is only displayed if the sex is selected as female', async () => {
+ const femaleOnlyFormFields = ['Pregnant?', 'LMP', 'Wants Planning?']
+
+ const dom = render(
+
+
+ ,
+ )
+
+ // ensure the female-only div is not displayed
+ femaleOnlyFormFields.forEach((fieldText) => {
+ expect(dom.queryByText(fieldText)).toBeNull()
+ })
+
+ // click the radio button for 'female'
+ const femaleButton = dom.getByTestId('female-button')
+ fireEvent.click(femaleButton)
+
+ femaleOnlyFormFields.forEach((fieldText) => {
+ expect(dom.queryByText(fieldText)).toBeTruthy()
+ })
+})
+
+test('sets visit input', () => {
+ const dom = render(
+
+
+ ,
+ )
+ const firstVisitYesInput = dom.queryByTestId('first-visit-Yes')
+ const checkBox = firstVisitYesInput.getElementsByTagName('span')[0]
+
+ expect(checkBox).not.toHaveClass('am-radio-checked')
+ fireEvent.click(firstVisitYesInput)
+ expect(checkBox).toHaveClass('am-radio-checked')
+})
+
+test('sets pregnant input', () => {
+ const dom = render(
+
+
+ ,
+ )
+ const femaleButton = dom.getByTestId('female-button')
+ fireEvent.click(femaleButton)
+
+ const pregnantYesInput = dom.queryByTestId('pregnant-Yes')
+ const checkBox = pregnantYesInput.getElementsByTagName('span')[0]
+
+ expect(checkBox).not.toHaveClass('am-radio-checked')
+ fireEvent.click(pregnantYesInput)
+ expect(checkBox).toHaveClass('am-radio-checked')
+})
+
+test('sets planning input', () => {
+ const dom = render(
+
+
+ ,
+ )
+ const femaleButton = dom.getByTestId('female-button')
+ fireEvent.click(femaleButton)
+
+ const planningYesInput = dom.queryByTestId('planning-Yes')
+ const checkBox = planningYesInput.getElementsByTagName('span')[0]
+
+ expect(checkBox).not.toHaveClass('am-radio-checked')
+ fireEvent.click(planningYesInput)
+ expect(checkBox).toHaveClass('am-radio-checked')
+})
+
+test('validates blood pressure input', () => {
+ const dom = render(
+
+
+ ,
+ )
+
+ const bpInput = dom.getByPlaceholderText(
+ 'Blood Pressure',
+ ) as HTMLInputElement
+ fireEvent.change(bpInput, { target: { value: '130/85' } })
+ expect(bpInput.value).toBe('130/85')
+ fireEvent.change(bpInput, { target: { value: '' } })
+ fireEvent.change(bpInput, { target: { value: 'abcd' } })
+ expect(bpInput.value).toBe('') // Empty String
+})
+
+test('sets albendazole input', () => {
+ const dom = render(
+
+
+ ,
+ )
+
+ const albendazoleYesInput = dom.queryByTestId('albendazole-Yes')
+ const checkBox = albendazoleYesInput.getElementsByTagName('span')[0]
+
+ expect(checkBox).not.toHaveClass('am-radio-checked')
+ fireEvent.click(albendazoleYesInput)
+ expect(checkBox).toHaveClass('am-radio-checked')
+})
+
+test('renders the Patient Info Form fields', () => {
+ const formFields = [
+ 'First Name',
+ 'First Name',
+ 'Last Name',
+ 'Last Name',
+ 'Sex',
+ 'Birth',
+ 'Age',
+ 'Phone',
+ 'Town',
+ 'Visit',
+ 'Nickname',
+ 'BP',
+ 'Temp',
+ 'Pulse',
+ 'Weight',
+ 'Height',
+ 'Z-Score',
+ 'Albendazole',
+ 'Allergies',
+ 'Medicines',
+ 'Chief Complaint',
+ 'History',
+ 'Exam',
+ ]
+
+ const { getByText } = render(
+
+
+ ,
+ )
+
+ formFields.forEach((fieldText) => {
+ const re = new RegExp(fieldText, 'gi')
+ expect(getByText(re)).toBeInTheDocument()
+ })
+})
diff --git a/pwa/src/components/PatientInfoForm.tsx b/pwa/src/components/PatientInfoForm.tsx
new file mode 100644
index 0000000..9530d4b
--- /dev/null
+++ b/pwa/src/components/PatientInfoForm.tsx
@@ -0,0 +1,388 @@
+import React, { ReactElement, useState } from 'react'
+import { useStoreActions } from '../state/storeIndex'
+import { SEX } from '../state/enums'
+import {
+ InputItem,
+ List,
+ TextareaItem,
+ Button,
+ Radio,
+ WhiteSpace,
+} from 'antd-mobile'
+import '../scss/PatientInfoForm.scss'
+
+const RadioItem = Radio.RadioItem
+
+export default function Form(): ReactElement {
+ // State Hooks for Radio Buttons
+ // useState returns a value, and a function to set that value
+ const [sex, setSex] = useState(null as number) // Default: null
+ const [albendazole, setAlbendazole] = useState(0) // Default: No
+ const [firstVisit, setFirstVisit] = useState(0) // Default: No
+ const [pregnant, setPregnant] = useState(0) // Default: No
+ const [planning, setPlanning] = useState(0) // Default: No
+ const [bloodPressureValue, setBloodPressureValue] = useState('')
+ const updateCentralState = useStoreActions(
+ (actions) => actions.patientInfoFormModel.updateFields,
+ )
+
+ // Data for Radio Buttons
+ const sexValues = [
+ { value: 0, label: 'Male' },
+ { value: 1, label: 'Female' },
+ ]
+
+ const radioYesNo = [
+ { value: 0, label: 'No' },
+ { value: 1, label: 'Yes' },
+ ]
+
+ /* istanbul ignore next */
+ const onSubmit = () => {
+ const firstNameField: HTMLInputElement = document.getElementById(
+ 'firstname',
+ ) as HTMLInputElement
+ const lastNameField: HTMLInputElement = document.getElementById(
+ 'lastname',
+ ) as HTMLInputElement
+ const nicknameField: HTMLInputElement = document.getElementById(
+ 'nickname',
+ ) as HTMLInputElement
+ const birthdateField: HTMLInputElement = document.getElementById(
+ 'birthdate',
+ ) as HTMLInputElement
+ const age: HTMLInputElement = document.getElementById(
+ 'age',
+ ) as HTMLInputElement
+ const phone: HTMLInputElement = document.getElementById(
+ 'phone',
+ ) as HTMLInputElement
+ const town: HTMLInputElement = document.getElementById(
+ 'town',
+ ) as HTMLInputElement
+ const gravida: HTMLInputElement = document.getElementById(
+ 'gravida',
+ ) as HTMLInputElement
+ const para: HTMLInputElement = document.getElementById(
+ 'para',
+ ) as HTMLInputElement
+ const abortus: HTMLInputElement = document.getElementById(
+ 'abortus',
+ ) as HTMLInputElement
+ const lmp: HTMLInputElement = document.getElementById(
+ 'lmp',
+ ) as HTMLInputElement
+ const weight: HTMLInputElement = document.getElementById(
+ 'weight',
+ ) as HTMLInputElement
+ const height: HTMLInputElement = document.getElementById(
+ 'height',
+ ) as HTMLInputElement
+ const temp: HTMLInputElement = document.getElementById(
+ 'temp',
+ ) as HTMLInputElement
+ const pulse: HTMLInputElement = document.getElementById(
+ 'pulse',
+ ) as HTMLInputElement
+ const zScore: HTMLInputElement = document.getElementById(
+ 'z-score',
+ ) as HTMLInputElement
+ const allergies: HTMLInputElement = document.getElementById(
+ 'allergies',
+ ) as HTMLInputElement
+ const medicines: HTMLInputElement = document.getElementById(
+ 'medicines',
+ ) as HTMLInputElement
+ const complaintField1: HTMLInputElement = document.getElementById(
+ 'complaint1',
+ ) as HTMLInputElement
+ const complaintField2: HTMLInputElement = document.getElementById(
+ 'complaint2',
+ ) as HTMLInputElement
+ const complaintField3: HTMLInputElement = document.getElementById(
+ 'complaint3',
+ ) as HTMLInputElement
+ const history: HTMLInputElement = document.getElementById(
+ 'history',
+ ) as HTMLInputElement
+ const exam: HTMLInputElement = document.getElementById(
+ 'exam',
+ ) as HTMLInputElement
+ updateCentralState({
+ firstName: firstNameField.value,
+ lastName: lastNameField.value,
+ nickname: nicknameField.value,
+ sex: sex ? SEX.female : SEX.male,
+ birthDate: new Date(birthdateField.value),
+ age: parseInt(age.value),
+ phone: phone.value,
+ town: town.value,
+ firstVisit: firstVisit,
+ pregnant: pregnant,
+ G: sex ? parseInt(gravida.value) : null,
+ P: sex ? parseInt(para.value) : null,
+ A: sex ? parseInt(abortus.value) : null,
+ LMP: sex ? new Date(lmp.value) : null,
+ wantsPlanning: planning,
+ weight: parseFloat(weight.value),
+ height: parseFloat(height.value),
+ bloodPressure: bloodPressureValue,
+ temperature: parseFloat(temp.value),
+ pulse: parseFloat(pulse.value),
+ zScore: parseFloat(zScore.value),
+ albendazole: albendazole,
+ allergies: allergies.value,
+ medicines: medicines.value,
+ chiefComplaint: [
+ complaintField1.value,
+ complaintField2.value,
+ complaintField3.value,
+ ],
+ history: history.value,
+ exam: exam.value,
+ })
+ }
+
+ const validateAndSetBloodPressure = (value: string) => {
+ const newChar = value.slice(-1)
+ if (newChar.match(/[0-9]|\/|^$/) && value.length < 8) {
+ setBloodPressureValue(value)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
'Sex'}>
+ {sexValues.map((i) => (
+ setSex(i.value)}
+ >
+ {i.label}
+
+ ))}
+
+
+
+
+
+ Birth Date
+
+
+
+ Age{' '}
+
+
+
+ Phone
+
+
+
+
+
'First Visit?'}>
+ {radioYesNo.map((i) => (
+ setFirstVisit(i.value)}
+ >
+ {i.label}
+
+ ))}
+
+
+ {/* Only display the 'female-only' div if the patient is female */}
+ {sex ? (
+
+
+ 'Pregnant?'}>
+ {radioYesNo.map((i) => (
+ setPregnant(i.value)}
+ >
+ {i.label}
+
+ ))}
+
+
+
+ G
+
+
+
+ P
+
+
+
+ A
+
+
+
+ LMP
+
+
+ 'Wants Planning?'}>
+ {radioYesNo.map((i) => (
+ setPlanning(i.value)}
+ >
+ {i.label}
+
+ ))}
+
+
+ ) : null}
+
+
+
+
+ Weight (kg)
+
+
+
+ Height (cm)
+
+
+
+ BP
+
+
+
+ Temp (F)
+
+
+
+ Pulse
+
+
+
+ Z-Score
+
+
+
'Albendazole?'}>
+ {radioYesNo.map((i) => (
+ setAlbendazole(i.value)}
+ >
+ {i.label}
+
+ ))}
+
+
+
'Allergies'}>
+
+
+
+
'Medicines'}>
+
+
+
+
'Chief Complaint'}>
+
+
+
+
+
+
'History'}>
+
+
+
+
'Exam'}>
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/pwa/src/hooks/useApi.test.tsx b/pwa/src/hooks/useApi.test.tsx
index 42286bf..17a3153 100644
--- a/pwa/src/hooks/useApi.test.tsx
+++ b/pwa/src/hooks/useApi.test.tsx
@@ -2,8 +2,8 @@ import { renderHook } from '@testing-library/react-hooks'
import useApi from './useApi'
test('should return loading and then a response', async () => {
- const { result, waitForNextUpdate } = renderHook(() => useApi(""))
-
+ const { result, waitForNextUpdate } = renderHook(() => useApi(''))
+
expect(result.current.response).toBeNull()
expect(result.current.error).toBeNull()
expect(result.current.isLoading).toBeTruthy()
@@ -13,4 +13,16 @@ test('should return loading and then a response', async () => {
expect(result.current.response).toBeTruthy()
expect(result.current.error).toBeNull()
expect(result.current.isLoading).toBeFalsy()
-})
\ No newline at end of file
+})
+
+test('should return an error on a non existent route', async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useApi('/404'))
+
+ expect(result.current.response).toBeNull()
+ expect(result.current.error).toBeNull()
+
+ await waitForNextUpdate()
+
+ expect(result.current.response).toBeFalsy()
+ expect(result.current.error).toBeTruthy()
+})
diff --git a/pwa/src/hooks/useApi.tsx b/pwa/src/hooks/useApi.tsx
index 50eb0cd..22629e5 100644
--- a/pwa/src/hooks/useApi.tsx
+++ b/pwa/src/hooks/useApi.tsx
@@ -1,35 +1,35 @@
import React from 'react'
const useApi = (route: string) => {
- const [response, setResponse] = React.useState(null);
- const [error, setError] = React.useState(null);
- const [isLoading, setIsLoading] = React.useState(false);
-
- /* istanbul ignore next */
- const getUrl = () => {
- if (process.env.environment === "docker"){
- return "http://api:4000/"
- }
- else {
- return "http://localhost:4000/"
- }
- }
+ const [response, setResponse] = React.useState(null)
+ const [error, setError] = React.useState(null)
+ const [isLoading, setIsLoading] = React.useState(false)
- React.useEffect(() => {
- const fetchData = async () => {
- setIsLoading(true);
- try {
- const res = await fetch(getUrl() + route);
- const json = await res.json();
- setResponse(json);
- setIsLoading(false)
- } catch (error) {
- setError(error);
- }
- };
- fetchData();
- }, []);
- return { response, error, isLoading };
-};
+ /* istanbul ignore next */
+ const getUrl = () => {
+ if (process.env.environment === 'docker') {
+ return 'http://api:4000/'
+ } else {
+ return 'http://localhost:4000/'
+ }
+ }
-export default useApi
\ No newline at end of file
+ React.useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true)
+ try {
+ const res = await fetch(getUrl() + route)
+ const json = await res.json()
+ setResponse(json)
+ setIsLoading(false)
+ } catch (error) {
+ setError(error)
+ }
+ }
+ fetchData()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+ return { response, error, isLoading }
+}
+
+export default useApi
diff --git a/pwa/src/index.tsx b/pwa/src/index.tsx
index 8284dd9..3ed6ea5 100644
--- a/pwa/src/index.tsx
+++ b/pwa/src/index.tsx
@@ -1,13 +1,13 @@
/* istanbul ignore file */
-import React from 'react';
-import ReactDOM from 'react-dom';
-import './scss/index.scss';
-import App from './App';
-import * as serviceWorker from './serviceWorker';
+import React from 'react'
+import ReactDOM from 'react-dom'
+import './scss/index.scss'
+import App from './App'
+import * as serviceWorker from './serviceWorker'
-ReactDOM.render(
, document.getElementById('root'));
+ReactDOM.render(
, document.getElementById('root'))
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
-serviceWorker.register();
+serviceWorker.register()
diff --git a/pwa/src/scss/App.scss b/pwa/src/scss/App.scss
index 9b04ce6..07cca34 100644
--- a/pwa/src/scss/App.scss
+++ b/pwa/src/scss/App.scss
@@ -1,7 +1,15 @@
.am-button:hover {
- cursor: pointer;
+ cursor: pointer;
}
#root {
- height: 100vh;
+ height: 100vh;
+}
+
+.am-input-label {
+ padding-right: 0.5em !important;
+}
+
+.am-textarea-label {
+ padding-right: 0.5em !important;
}
diff --git a/pwa/src/scss/LandingPage.scss b/pwa/src/scss/LandingPage.scss
index 905257e..ba4a0a3 100644
--- a/pwa/src/scss/LandingPage.scss
+++ b/pwa/src/scss/LandingPage.scss
@@ -1,21 +1,19 @@
.button-flex {
- animation-name: fade-in;
- animation-duration: 2s;
- display: flex;
- justify-content: center;
- margin: 2em 0;
- .am-button {
- background-color: #9bd7ff;
- width: 40%;
- &.am-button-active {
- background-color: #ccebff;
+ display: flex;
+ justify-content: center;
+ margin: 2em 0;
+ .am-button {
+ background-color: #9bd7ff;
+ width: 40%;
+ &.am-button-active {
+ background-color: #ccebff;
+ }
}
- }
}
#landing-wrapper {
- display: flex;
- flex-direction: column;
- justify-content: center;
- height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 100%;
}
diff --git a/pwa/src/scss/Login.scss b/pwa/src/scss/Login.scss
index ea78e88..94a0286 100644
--- a/pwa/src/scss/Login.scss
+++ b/pwa/src/scss/Login.scss
@@ -2,70 +2,78 @@
// @include media("
$no-media-breakpoint-value) {
- @return false;
- }
- } @else if not index($im-no-media-expressions, $condition) {
- @return false;
+ @if not $no-media-breakpoint-value {
+ @if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {
+ }
}
- }
- @return true;
+ @each $condition in $conditions {
+ @if not map-has-key($media-expressions, $condition) {
+ $operator: get-expression-operator($condition);
+ $prefix: get-expression-prefix($operator);
+ $value: get-expression-value($condition, $operator);
+
+ @if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
+ ($prefix == 'min' and $value > $no-media-breakpoint-value)
+ {
+ @return false;
+ }
+ } @else if not index($im-no-media-expressions, $condition) {
+ @return false;
+ }
+ }
+
+ @return true;
}
////
@@ -252,7 +252,6 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @access private
////
-
///
/// Get operator of an expression
///
@@ -261,21 +260,20 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
///
@function get-expression-operator($expression) {
- @each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
- @if str-index($expression, $operator) {
- @return $operator;
+ @each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
+ @if str-index($expression, $operator) {
+ @return $operator;
+ }
}
- }
- // It is not possible to include a mixin inside a function, so we have to
- // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
- // functions cannot be called anywhere in Sass, we need to hack the call in
- // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
- // Sass 3.3, change this line in `@if im-log(..) {}` instead.
- $_: im-log('No operator found in `#{$expression}`.');
+ // It is not possible to include a mixin inside a function, so we have to
+ // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
+ // functions cannot be called anywhere in Sass, we need to hack the call in
+ // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
+ // Sass 3.3, change this line in `@if im-log(..) {}` instead.
+ $_: im-log('No operator found in `#{$expression}`.');
}
-
///
/// Get dimension of an expression, based on a found operator
///
@@ -285,18 +283,17 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {String} - `width` or `height` (or potentially anything else)
///
@function get-expression-dimension($expression, $operator) {
- $operator-index: str-index($expression, $operator);
- $parsed-dimension: str-slice($expression, 0, $operator-index - 1);
- $dimension: 'width';
+ $operator-index: str-index($expression, $operator);
+ $parsed-dimension: str-slice($expression, 0, $operator-index - 1);
+ $dimension: 'width';
- @if str-length($parsed-dimension) > 0 {
- $dimension: $parsed-dimension;
- }
+ @if str-length($parsed-dimension) > 0 {
+ $dimension: $parsed-dimension;
+ }
- @return $dimension;
+ @return $dimension;
}
-
///
/// Get dimension prefix based on an operator
///
@@ -305,10 +302,9 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {String} - `min` or `max`
///
@function get-expression-prefix($operator) {
- @return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
+ @return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
}
-
///
/// Get value of an expression, based on a found operator
///
@@ -318,36 +314,35 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {Number} - A numeric value
///
@function get-expression-value($expression, $operator) {
- $operator-index: str-index($expression, $operator);
- $value: str-slice($expression, $operator-index + str-length($operator));
+ $operator-index: str-index($expression, $operator);
+ $value: str-slice($expression, $operator-index + str-length($operator));
- @if map-has-key($breakpoints, $value) {
- $value: map-get($breakpoints, $value);
- } @else {
- $value: to-number($value);
- }
+ @if map-has-key($breakpoints, $value) {
+ $value: map-get($breakpoints, $value);
+ } @else {
+ $value: to-number($value);
+ }
- $interval: map-get($unit-intervals, unit($value));
+ $interval: map-get($unit-intervals, unit($value));
- @if not $interval {
- // It is not possible to include a mixin inside a function, so we have to
- // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
- // functions cannot be called anywhere in Sass, we need to hack the call in
- // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
- // Sass 3.3, change this line in `@if im-log(..) {}` instead.
- $_: im-log('Unknown unit `#{unit($value)}`.');
- }
+ @if not $interval {
+ // It is not possible to include a mixin inside a function, so we have to
+ // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
+ // functions cannot be called anywhere in Sass, we need to hack the call in
+ // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
+ // Sass 3.3, change this line in `@if im-log(..) {}` instead.
+ $_: im-log('Unknown unit `#{unit($value)}`.');
+ }
- @if $operator == '>' {
- $value: $value + $interval;
- } @else if $operator == '<' {
- $value: $value - $interval;
- }
+ @if $operator == '>' {
+ $value: $value + $interval;
+ } @else if $operator == '<' {
+ $value: $value - $interval;
+ }
- @return $value;
+ @return $value;
}
-
///
/// Parse an expression to return a valid media-query expression
///
@@ -356,18 +351,18 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {String} - Valid media query
///
@function parse-expression($expression) {
- // If it is part of $media-expressions, it has no operator
- // then there is no need to go any further, just return the value
- @if map-has-key($media-expressions, $expression) {
- @return map-get($media-expressions, $expression);
- }
+ // If it is part of $media-expressions, it has no operator
+ // then there is no need to go any further, just return the value
+ @if map-has-key($media-expressions, $expression) {
+ @return map-get($media-expressions, $expression);
+ }
- $operator: get-expression-operator($expression);
- $dimension: get-expression-dimension($expression, $operator);
- $prefix: get-expression-prefix($operator);
- $value: get-expression-value($expression, $operator);
+ $operator: get-expression-operator($expression);
+ $dimension: get-expression-dimension($expression, $operator);
+ $prefix: get-expression-prefix($operator);
+ $value: get-expression-value($expression, $operator);
- @return '(#{$prefix}-#{$dimension}: #{$value})';
+ @return '(#{$prefix}-#{$dimension}: #{$value})';
}
///
@@ -382,17 +377,17 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {List} Sliced list
///
@function slice($list, $start: 1, $end: length($list)) {
- @if length($list) < 1 or $start > $end {
- @return ();
- }
+ @if length($list) < 1 or $start > $end {
+ @return ();
+ }
- $result: ();
+ $result: ();
- @for $i from $start through $end {
- $result: append($result, nth($list, $i));
- }
+ @for $i from $start through $end {
+ $result: append($result, nth($list, $i));
+ }
- @return $result;
+ @return $result;
}
////
@@ -401,7 +396,6 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @access private
////
-
///
/// Casts a string into a number
///
@@ -410,44 +404,57 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {Number}
///
@function to-number($value) {
- @if type-of($value) == 'number' {
- @return $value;
- } @else if type-of($value) != 'string' {
- $_: im-log('Value for `to-number` should be a number or a string.');
- }
-
- $first-character: str-slice($value, 1, 1);
- $result: 0;
- $digits: 0;
- $minus: ($first-character == '-');
- $numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
-
- // Remove +/- sign if present at first character
- @if ($first-character == '+' or $first-character == '-') {
- $value: str-slice($value, 2);
- }
-
- @for $i from 1 through str-length($value) {
- $character: str-slice($value, $i, $i);
-
- @if not (index(map-keys($numbers), $character) or $character == '.') {
- @return to-length(if($minus, -$result, $result), str-slice($value, $i))
+ @if type-of($value) == 'number' {
+ @return $value;
+ } @else if type-of($value) != 'string' {
+ $_: im-log('Value for `to-number` should be a number or a string.');
}
- @if $character == '.' {
- $digits: 1;
- } @else if $digits == 0 {
- $result: $result * 10 + map-get($numbers, $character);
- } @else {
- $digits: $digits * 10;
- $result: $result + map-get($numbers, $character) / $digits;
- }
- }
+ $first-character: str-slice($value, 1, 1);
+ $result: 0;
+ $digits: 0;
+ $minus: ($first-character == '-');
+ $numbers: (
+ '0': 0,
+ '1': 1,
+ '2': 2,
+ '3': 3,
+ '4': 4,
+ '5': 5,
+ '6': 6,
+ '7': 7,
+ '8': 8,
+ '9': 9,
+ );
- @return if($minus, -$result, $result);
+ // Remove +/- sign if present at first character
+ @if ($first-character == '+' or $first-character == '-') {
+ $value: str-slice($value, 2);
+ }
+
+ @for $i from 1 through str-length($value) {
+ $character: str-slice($value, $i, $i);
+
+ @if not(index(map-keys($numbers), $character) or $character == '.') {
+ @return to-length(
+ if($minus, -$result, $result),
+ str-slice($value, $i)
+ );
+ }
+
+ @if $character == '.' {
+ $digits: 1;
+ } @else if $digits == 0 {
+ $result: $result * 10 + map-get($numbers, $character);
+ } @else {
+ $digits: $digits * 10;
+ $result: $result + map-get($numbers, $character) / $digits;
+ }
+ }
+
+ @return if($minus, -$result, $result);
}
-
///
/// Add `$unit` to `$value`
///
@@ -457,13 +464,29 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @return {Number} - `$value` expressed in `$unit`
///
@function to-length($value, $unit) {
- $units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
+ $units: (
+ 'px': 1px,
+ 'cm': 1cm,
+ 'mm': 1mm,
+ '%': 1%,
+ 'ch': 1ch,
+ 'pc': 1pc,
+ 'in': 1in,
+ 'em': 1em,
+ 'rem': 1rem,
+ 'pt': 1pt,
+ 'ex': 1ex,
+ 'vw': 1vw,
+ 'vh': 1vh,
+ 'vmin': 1vmin,
+ 'vmax': 1vmax,
+ );
- @if not index(map-keys($units), $unit) {
- $_: im-log('Invalid unit `#{$unit}`.');
- }
+ @if not index(map-keys($units), $unit) {
+ $_: im-log('Invalid unit `#{$unit}`.');
+ }
- @return $value * map-get($units, $unit);
+ @return $value * map-get($units, $unit);
}
///
@@ -505,19 +528,22 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// }
///
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
- // Save global configuration
- $global-breakpoints: $breakpoints;
- $global-media-expressions: $media-expressions;
+ // Save global configuration
+ $global-breakpoints: $breakpoints;
+ $global-media-expressions: $media-expressions;
- // Update global configuration
- $breakpoints: map-merge($breakpoints, $tweakpoints) !global;
- $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
+ // Update global configuration
+ $breakpoints: map-merge($breakpoints, $tweakpoints) !global;
+ $media-expressions: map-merge(
+ $media-expressions,
+ $tweak-media-expressions
+ ) !global;
- @content;
+ @content;
- // Restore global configuration
- $breakpoints: $global-breakpoints !global;
- $media-expressions: $global-media-expressions !global;
+ // Restore global configuration
+ $breakpoints: $global-breakpoints !global;
+ $media-expressions: $global-media-expressions !global;
}
////
@@ -526,7 +552,6 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @access public
////
-
///
/// Generates a media query based on a list of conditions
///
@@ -551,15 +576,20 @@ $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
/// @include media('>=350px', ' 0) {
- @media #{unquote(parse-expression(nth($conditions, 1)))} {
- // Recursive call
- @include media(slice($conditions, 2)...) {
+ @if ($im-media-support and length($conditions) == 0) or
+ (
+ not
+ $im-media-support and
+ im-intercepts-static-breakpoint($conditions...)
+ )
+ {
@content;
- }
+ } @else if ($im-media-support and length($conditions) > 0) {
+ @media #{unquote(parse-expression(nth($conditions, 1)))} {
+ // Recursive call
+ @include media(slice($conditions, 2) ...) {
+ @content;
+ }
+ }
}
- }
}
diff --git a/pwa/src/scss/index.scss b/pwa/src/scss/index.scss
index 8d78e58..995b512 100644
--- a/pwa/src/scss/index.scss
+++ b/pwa/src/scss/index.scss
@@ -1,16 +1,16 @@
body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- background-color: #e8ffd9;
- height: 100%;
- width: 100%;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
+ 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
+ 'Helvetica Neue', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #e8ffd9;
+ height: 100%;
+ width: 100%;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
\ No newline at end of file
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/pwa/src/serviceWorker.ts b/pwa/src/serviceWorker.ts
index 4592733..b877ff6 100644
--- a/pwa/src/serviceWorker.ts
+++ b/pwa/src/serviceWorker.ts
@@ -1,4 +1,5 @@
/* istanbul ignore file */
+/* eslint-disable */
// This optional code is used to register a service worker.
// register() is not called by default.
@@ -12,135 +13,133 @@
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
- window.location.hostname === 'localhost' ||
- // [::1] is the IPv6 localhost address.
- window.location.hostname === '[::1]' ||
- // 127.0.0.0/8 are considered localhost for IPv4.
- window.location.hostname.match(
- /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
- )
-);
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.0/8 are considered localhost for IPv4.
+ window.location.hostname.match(
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
+ ),
+)
type Config = {
- onSuccess?: (registration: ServiceWorkerRegistration) => void;
- onUpdate?: (registration: ServiceWorkerRegistration) => void;
-};
+ onSuccess?: (registration: ServiceWorkerRegistration) => void
+ onUpdate?: (registration: ServiceWorkerRegistration) => void
+}
export function register(config?: Config) {
- if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
- // The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(
- process.env.PUBLIC_URL,
- window.location.href
- );
- if (publicUrl.origin !== window.location.origin) {
- // Our service worker won't work if PUBLIC_URL is on a different origin
- // from what our page is served on. This might happen if a CDN is used to
- // serve assets; see https://github.com/facebook/create-react-app/issues/2374
- return;
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config)
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://bit.ly/CRA-PWA',
+ )
+ })
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config)
+ }
+ })
}
-
- window.addEventListener('load', () => {
- const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
-
- if (isLocalhost) {
- // This is running on localhost. Let's check if a service worker still exists or not.
- checkValidServiceWorker(swUrl, config);
-
- // Add some additional logging to localhost, pointing developers to the
- // service worker/PWA documentation.
- navigator.serviceWorker.ready.then(() => {
- console.log(
- 'This web app is being served cache-first by a service ' +
- 'worker. To learn more, visit https://bit.ly/CRA-PWA'
- );
- });
- } else {
- // Is not localhost. Just register service worker
- registerValidSW(swUrl, config);
- }
- });
- }
}
function registerValidSW(swUrl: string, config?: Config) {
- navigator.serviceWorker
- .register(swUrl)
- .then(registration => {
- registration.onupdatefound = () => {
- const installingWorker = registration.installing;
- if (installingWorker == null) {
- return;
- }
- installingWorker.onstatechange = () => {
- if (installingWorker.state === 'installed') {
- if (navigator.serviceWorker.controller) {
- // At this point, the updated precached content has been fetched,
- // but the previous service worker will still serve the older
- // content until all client tabs are closed.
- console.log(
- 'New content is available and will be used when all ' +
- 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
- );
+ navigator.serviceWorker
+ .register(swUrl)
+ .then((registration) => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing
+ if (installingWorker == null) {
+ return
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
+ )
- // Execute callback
- if (config && config.onUpdate) {
- config.onUpdate(registration);
- }
- } else {
- // At this point, everything has been precached.
- // It's the perfect time to display a
- // "Content is cached for offline use." message.
- console.log('Content is cached for offline use.');
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration)
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.')
- // Execute callback
- if (config && config.onSuccess) {
- config.onSuccess(registration);
- }
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration)
+ }
+ }
+ }
+ }
}
- }
- };
- };
- })
- .catch(error => {
- console.error('Error during service worker registration:', error);
- });
+ })
+ .catch((error) => {
+ console.error('Error during service worker registration:', error)
+ })
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
- // Check if the service worker can be found. If it can't reload the page.
- fetch(swUrl, {
- headers: { 'Service-Worker': 'script' }
- })
- .then(response => {
- // Ensure service worker exists, and that we really are getting a JS file.
- const contentType = response.headers.get('content-type');
- if (
- response.status === 404 ||
- (contentType != null && contentType.indexOf('javascript') === -1)
- ) {
- // No service worker found. Probably a different app. Reload the page.
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister().then(() => {
- window.location.reload();
- });
- });
- } else {
- // Service worker found. Proceed as normal.
- registerValidSW(swUrl, config);
- }
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { 'Service-Worker': 'script' },
})
- .catch(() => {
- console.log(
- 'No internet connection found. App is running in offline mode.'
- );
- });
+ .then((response) => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type')
+ if (
+ response.status === 404 ||
+ (contentType != null &&
+ contentType.indexOf('javascript') === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister().then(() => {
+ window.location.reload()
+ })
+ })
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config)
+ }
+ })
+ .catch(() => {
+ console.log(
+ 'No internet connection found. App is running in offline mode.',
+ )
+ })
}
export function unregister() {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.ready.then(registration => {
- registration.unregister();
- });
- }
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister()
+ })
+ }
}
diff --git a/pwa/src/setupTests.ts b/pwa/src/setupTests.ts
index 74b1a27..2eb59b0 100644
--- a/pwa/src/setupTests.ts
+++ b/pwa/src/setupTests.ts
@@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom/extend-expect';
+import '@testing-library/jest-dom/extend-expect'
diff --git a/pwa/src/state/enums.ts b/pwa/src/state/enums.ts
new file mode 100644
index 0000000..f26a865
--- /dev/null
+++ b/pwa/src/state/enums.ts
@@ -0,0 +1,13 @@
+/* istanbul ignore file */
+
+export enum SEX {
+ male = 'male',
+ female = 'female',
+}
+
+export enum YN {
+ YES = 1,
+ NO = 0,
+}
+
+export default {}
diff --git a/pwa/src/state/models/patientInfoForm.model.ts b/pwa/src/state/models/patientInfoForm.model.ts
new file mode 100644
index 0000000..e9aefd7
--- /dev/null
+++ b/pwa/src/state/models/patientInfoForm.model.ts
@@ -0,0 +1,48 @@
+import { Action, action } from 'easy-peasy'
+import { SEX, YN } from '../enums'
+
+interface PatientInfoFormFields {
+ firstName?: string
+ lastName?: string
+ nickname?: string
+ sex?: SEX
+ birthDate?: Date
+ age?: number
+ phone?: string
+ town?: string
+ firstVisit?: YN
+ pregnant?: YN
+ G?: number
+ P?: number
+ A?: number
+ LMP?: Date
+ wantsPlanning?: YN
+ weight?: number
+ height?: number
+ bloodPressure?: string
+ temperature?: number
+ pulse?: number
+ zScore?: number
+ albendazole?: YN
+ allergies?: string
+ medicines?: string
+ chiefComplaint?: string[]
+ history?: string
+ exam?: string
+}
+
+export interface PatientInfoFormModel {
+ fields: PatientInfoFormFields
+ updateFields: Action
+}
+
+/* istanbul ignore next */
+// Form submission requests validated in integration test
+const patientInfoFormModel: PatientInfoFormModel = {
+ fields: {},
+ updateFields: action((state, formObject) => {
+ state.fields = formObject
+ }),
+}
+
+export default patientInfoFormModel
diff --git a/pwa/src/state/store.ts b/pwa/src/state/store.ts
new file mode 100644
index 0000000..7142739
--- /dev/null
+++ b/pwa/src/state/store.ts
@@ -0,0 +1,8 @@
+/* istanbul ignore file */
+
+import { createStore, persist } from 'easy-peasy'
+import storeIndex from './storeIndex'
+
+const store = createStore(persist(storeIndex))
+
+export default store
diff --git a/pwa/src/state/storeIndex.ts b/pwa/src/state/storeIndex.ts
new file mode 100644
index 0000000..b77da81
--- /dev/null
+++ b/pwa/src/state/storeIndex.ts
@@ -0,0 +1,17 @@
+/* istanbul ignore file */
+import { createTypedHooks } from 'easy-peasy'
+
+import patientInfoFormModel from './models/patientInfoForm.model'
+
+export const storeModels = {
+ patientInfoFormModel,
+}
+
+export const {
+ useStore,
+ useStoreState,
+ useStoreActions,
+ useStoreDispatch,
+} = createTypedHooks()
+
+export default storeModels