Fakultas Ilmu Komputer UI

Commit 7c265074 authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Merge branch 'PBI-9-diet_questionnaire_ui_and_validation' into 'staging'

Implement diet questionnaire UI layout and form validation

See merge request !41
parents 012d6878 0be41e76
Pipeline #75934 passed with stages
in 84 minutes and 46 seconds
export interface Choice {
label: string;
value: number;
value: number | string;
}
export interface Props {
......
import React from 'react';
import { render } from '@testing-library/react-native';
import Picker from '.';
describe('Picker component', () => {
let value: any = null;
const props = {
choices: [
{
label: 'Choice 1',
value: 1,
},
{
label: 'Choice 2',
value: 2,
},
],
value: value,
onChange: (v: any) => (value = v),
};
it('renders correctly', () => {
render(<Picker {...props} />);
});
it('renders correctly with error message', () => {
render(<Picker {...props} errorMessage="error" />);
});
});
import React, { FC } from 'react';
import { Picker as RNPicker } from '@react-native-picker/picker';
import { View } from 'react-native';
import { Text } from 'react-native-elements';
import { typographyStyles } from 'styles';
import { styles } from './styles';
import { Props } from './types';
import FormLabel from '../FormLabel';
const Picker: FC<Props> = ({
label,
required,
placeholder,
errorMessage,
choices,
value,
onChange,
}) => (
<View>
<FormLabel label={label} required={required} />
<View style={styles.container}>
<RNPicker selectedValue={value} onValueChange={(v, _) => onChange(v)}>
<RNPicker.Item
label={placeholder || `Pilih ${label?.toLowerCase()}`}
value={0}
style={styles.placeholder}
/>
{choices.map((choice) => (
<RNPicker.Item
key={choice.value}
label={choice.label}
value={choice.value}
/>
))}
</RNPicker>
</View>
{errorMessage ? (
<Text style={[typographyStyles.caption, styles.red]}>{errorMessage}</Text>
) : null}
</View>
);
export default Picker;
import { StyleSheet } from 'react-native';
import { colors } from 'styles';
export const styles = StyleSheet.create({
red: {
color: 'red',
marginLeft: 4,
marginTop: 4,
},
container: {
borderRadius: 4,
borderWidth: 1,
borderColor: colors.border,
},
placeholder: {
color: colors.formLabel,
},
});
import { Choice } from '../MultipleChoice/types';
import { Props as FormLabelProps } from '../FormLabel/types';
export interface Props extends FormLabelProps {
placeholder?: string;
errorMessage?: any;
choices: Choice[];
value: any;
onChange: (_: any) => void;
}
......@@ -21,17 +21,14 @@ export const RadioButtonGroup: FC<RadioButtonGroupProps> = ({
}) => (
<View>
<FormLabel label={label} required={required} />
<View style={styles.container}>
{choices.map((choice) => (
<RadioButton
key={choice.value}
title={choice.label}
checked={value === choice.value}
onPress={() => onChange(choice.value)}
containerStyle={styles.radioButton}
/>
))}
</View>
{choices.map((choice) => (
<RadioButton
key={choice.value}
title={choice.label}
checked={value === choice.value}
onPress={() => onChange(choice.value)}
/>
))}
{errorMessage ? (
<Text style={[typographyStyles.caption, styles.red]}>{errorMessage}</Text>
) : null}
......
import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
flex: 1,
},
radioButton: {
flex: 0.48,
},
red: {
color: 'red',
},
......
......@@ -20,7 +20,6 @@ describe('StepByStepForm component', () => {
name,
route: name,
})),
title: 'title',
currentPage: 0,
finishRedirectRoute: 'finishroute',
};
......@@ -34,7 +33,7 @@ describe('StepByStepForm component', () => {
it('has "Ubah" button when form has been filled', () => {
const { queryByText } = render(
<StepByStepForm {...props} currentPage={1} />,
<StepByStepForm {...props} currentPage={2} />,
);
expect(queryByText(/Ubah/i)).toBeTruthy();
});
......
import React, { FC } from 'react';
import { useNavigation } from '@react-navigation/native';
import { View } from 'react-native';
import { Button, Text } from 'react-native-elements';
import { View, ScrollView } from 'react-native';
import { Button, Text, Icon } from 'react-native-elements';
import { styles } from './styles';
import { Props } from './types';
import { colors } from 'styles';
const StepByStepForm: FC<Props> = ({
title,
pages,
currentPage,
defaultValues,
......@@ -16,6 +16,9 @@ const StepByStepForm: FC<Props> = ({
const navigation = useNavigation();
const getButton = (i: number, route: string) => {
if (i === 0 && currentPage !== 0) {
return <Icon name="lock" color={colors.primaryYellow} size={30} />;
}
let buttonLabel;
if (i < currentPage) {
buttonLabel = 'Ubah';
......@@ -33,10 +36,7 @@ const StepByStepForm: FC<Props> = ({
};
return (
<View style={styles.container}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text>
</View>
<ScrollView contentContainerStyle={styles.container}>
<View style={styles.pageContainer}>
{pages.map((page, i) => (
<View style={styles.page} key={i}>
......@@ -60,7 +60,7 @@ const StepByStepForm: FC<Props> = ({
</View>
))}
</View>
<View style={styles.titleContainer}>
<View style={styles.bottomContainer}>
<Button
title="Selesai"
onPress={() => navigation.navigate(finishRedirectRoute)}
......@@ -69,7 +69,7 @@ const StepByStepForm: FC<Props> = ({
disabled={currentPage !== pages.length}
/>
</View>
</View>
</ScrollView>
);
};
......
......@@ -3,21 +3,18 @@ import { colors, typography } from 'styles';
export const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 20,
},
titleContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
alignItems: 'center',
flex: 1,
paddingHorizontal: 20,
},
title: {
...typography.headingLarge,
textAlign: 'center',
bottomContainer: {
marginTop: 20,
},
pageContainer: {
flex: 3,
display: 'flex',
alignItems: 'center',
},
notfilled: {
color: colors.formLabel,
......
export interface Props {
title: string;
pages: {
name: string;
route: string;
......
......@@ -14,6 +14,7 @@ const TextField: FC<Props> = ({ label, required, errorMessage, ...props }) => (
errorMessage ? styles.bigMargin : styles.smallMargin,
]}
errorMessage={errorMessage}
inputContainerStyle={styles.inputContainerStyle}
/>
);
......
import { StyleSheet } from 'react-native';
import { colors } from 'styles';
export const styles = StyleSheet.create({
red: {
......@@ -10,4 +11,10 @@ export const styles = StyleSheet.create({
bigMargin: {
height: 30,
},
inputContainerStyle: {
borderColor: colors.border,
borderWidth: 1,
borderRadius: 4,
paddingHorizontal: 10,
},
});
import { InputProps } from 'react-native-elements';
export interface Props extends InputProps {
label?: string;
required?: boolean;
errorMessage?: any;
}
export { default as CheckboxGroup } from './CheckboxGroup';
export { default as Datepicker } from './Datepicker';
export { default as MultipleCheckbox } from './MultipleCheckbox';
export { default as MultipleChoice } from './MultipleChoice';
export { default as Picker } from './Picker';
export { default as RadioButton, RadioButtonGroup } from './RadioButton';
export { default as StepByStepForm } from './StepByStepForm';
export { default as TextField } from './TextField';
......@@ -14,6 +14,14 @@ import {
ExtendedQuestionnaire,
} from 'scenes';
import { FC } from 'react';
import {
ConsentForm,
Questionnaire1,
Questionnaire2,
Questionnaire3,
Questionnaire4,
Questionnaire5,
} from 'scenes/questionnaire/ExtendedQuestionnaire/components';
export interface NavRoute {
name: string;
......@@ -81,22 +89,55 @@ export const unpaidClientNavigation: NavRoute[] = [
...navigation,
];
export const privateNavigation: NavRoute[] = [
{
name: ROUTES.profile,
component: ComingSoonPage,
header: 'Profile',
},
export const onboardingClientNavigation: NavRoute[] = [
{
name: ROUTES.extendedQuestionnaire,
component: ExtendedQuestionnaire,
header: 'Diet Questionnaire',
header: 'Profil Saya',
},
...[
{
component: ConsentForm,
header: 'Persetujuan',
},
{
component: Questionnaire1,
header: 'Identitas Diri',
},
{
component: Questionnaire2,
header: 'Pola Makan',
},
{
component: Questionnaire3,
header: 'Konsumsi Makan',
},
{
component: Questionnaire4,
header: 'Gaya Hidup',
},
{
component: Questionnaire5,
header: 'Kondisi Pribadi',
},
].map((nav, id) => ({
...nav,
name: ROUTES.extendedQuestionnaireById(id),
})),
];
export const clientNavigation: NavRoute[] = [
{
name: ROUTES.clientProfile,
component: ComingSoonPage,
header: 'Profile',
},
...onboardingClientNavigation,
];
export const testNavigation: NavRoute[] = [
...navigation,
...privateNavigation,
...onboardingClientNavigation,
{
name: ROUTES.initial,
component: InitialPage,
......
export const yesOrNo = ['Ya', 'Tidak'];
export const drinkFrequency = [
'Tidak pernah',
'Sangat jarang',
'Tidak lebih dari 3 gelas per bulan',
'1 gelas per minggu',
'2 gelas atau lebih per minggu',
'Hampir setiap hari',
'1 gelas (250 ml) per hari',
'2 gelas (500 ml) per hari',
'Lebih dari 2 gelas per hari',
];
export const checkbox = {
OTHER: 'OTHER',
};
import { yesOrNo, drinkFrequency } from 'constants/options';
import { TextFieldSchema } from 'types/form';
export const pageHeaders: string[] = [
'Persetujuan Program Diet',
'Identitas Diri',
'Pola Makan',
'Konsumsi Makanan Harian',
'Gaya Hidup dan Kebiasaan Diet',
'Kondisi Pribadi',
];
export const textFields: { [_: string]: TextFieldSchema[] } = {
identity: [
{
label: 'Daerah Tempat Tinggal',
placeholder: 'Tebet, Jakarta',
name: 'city_and_area_of_residence',
required: true,
},
{
label: 'Nomor HP',
placeholder: 'Ex: 081234567890',
name: 'handphone_no',
required: true,
keyboardType: 'numeric',
},
{
label: 'Nomor WhatsApp',
placeholder: 'Kosongkan jika sama dengan nomor HP',
name: 'whatsapp_no',
keyboardType: 'numeric',
},
{
label: 'Lingkar Pinggang (cm)',
placeholder: 'Ex: 100',
name: 'waist_size',
required: true,
keyboardType: 'numeric',
},
],
eatingPattern: [
{
label: 'Makanan apa yang hampir setiap hari Anda konsumsi?',
placeholder: '-',
name: 'meal_consumed_almost_every_day',
errorMessage: 'Makanan yang dikonsumsi',
},
{
label: 'Makanan apa yang tidak Anda sukai?',
placeholder: '-',
name: 'unliked_food',
errorMessage: 'Makanan yang tidak disukai',
},
{
label:
'Adakah cita rasa makanan atau jenis makanan tertentu yang Anda sukai?',
placeholder: 'Chinese food',
name: 'preferred_food_taste',
errorMessage: 'Cita rasa yang disukai',
},
{
label: 'Makanan apa saja yang Anda harapkan muncul di menu sarapan?',
placeholder: 'Telur',
name: 'expected_food_on_breakfast',
errorMessage: 'Makanan untuk sarapan',
},
{
label:
'Makanan apa saja yang Anda harapkan muncul di menu makan siang dan makan malam?',
placeholder: 'Ayam',
name: 'expected_food_on_lunch_dinner',
errorMessage: 'Makanan untuk makan siang dan malam',
},
],
foodConsumption: [
{
label: 'Sarapan (jam 00.00 - 10.00)',
placeholder: 'Sesuai format jawaban',
name: 'breakfast_meal_explanation',
},
{
label: 'Cemilan pagi (jam 10.00 - 12.00)',
placeholder: 'Sesuai format jawaban',
name: 'morning_snack_explanation',
},
{
label: 'Makan siang (jam 12.00 - 15.00)',
placeholder: 'Sesuai format jawaban',
name: 'lunch_meal_explanation',
},
{
label: 'Cemilan sore (jam 15.00 - 18.00)',
placeholder: 'Sesuai format jawaban',
name: 'evening_snack_explanation',
},
{
label: 'Makan malam (jam 18.00 - 21.00)',
placeholder: 'Sesuai format jawaban',
name: 'dinner_meal_explanation',
},
{
label: 'Cemilan malam (jam 21.00 - 00.00)',
placeholder: 'Sesuai format jawaban',
name: 'night_snack_explanation',
},
],
lifestyle: [
{
label: 'Makanan yang menyebabkan Anda alergi (jika ada)',
placeholder: 'Ayam',
name: 'food_alergies',
},
{
label:
'Jenis obat atau ramuan herbal atau minuman khusus yang pernah atau sedang Anda gunakan untuk diet (jika ada)',
placeholder: 'TCM',
name: 'diet_drinks',
},
{
label:
'Jenis multivitamin atau tablet suplementasi yang Anda konsumsi (jika ada)',
placeholder: 'Natur-E',
name: 'multivitamin_tablet_suplement',
},
{
label:
'Tuliskan hal lain yang ingin Anda ceritakan tentang pola makan atau gaya hidup Anda yang dapat membantu kami dalam mempertimbangkan saran bagi Anda?',
multiline: true,
name: 'diet_and_life_style_story',
},
],
healthCondition: [
{
label:
'Adakah konsumsi obat-obatan rutin yang perlu kami ketahui? Jika ada, mohon sebutkan obat untuk apa atau merk obatnya.',
placeholder:
'Contoh: obat diabetes, obat hipertensi, dexamethasone, metformin, captopril',
name: 'regular_drug_consumption',
},
{
label:
'Adakah riwayat penyakit atau kondisi kesehatan lainnya yang perlu kami ketahui?',
name: 'other_disease',
},
{
label: 'Apa motivasi Anda menggunakan program Dietela?',
name: 'motivation_using_dietela',
},
{
label: 'Apa yang Anda harapkan dari Nutrisionis Dietela?',
name: 'dietela_nutritionist_expectation',
},
{
label: 'Apa yang Anda harapkan dari program yang Anda pilih?',
name: 'dietela_program_expectation',
},
],
};
export const selectFields = {
identity: [
{
name: 'profession',
label: 'Pekerjaan',
picker: true,
choiceList: [
'Pegawai Swasta',
'PNS',
'Wirausaha',
'Self-Employed',
'Freelancer',
'Ibu Rumah Tangga',
'Mahasiswa',
'Pelajar',
'Lainnya',
],
},
{
name: 'last_education',
label: 'Pendidikan Terakhir',