Fakultas Ilmu Komputer UI

Commit 1f89cc9d authored by Wulan Mantiri's avatar Wulan Mantiri
Browse files

Implement Choose Plan UI layout and integrate with API

parent a372f3fa
...@@ -63,3 +63,4 @@ buck-out/ ...@@ -63,3 +63,4 @@ buck-out/
/ios/Pods/ /ios/Pods/
coverage/ coverage/
.husky
...@@ -7,6 +7,6 @@ module.exports = { ...@@ -7,6 +7,6 @@ module.exports = {
], ],
moduleNameMapper: { moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'identity-obj-proxy', 'jest-transform-stub',
}, },
}; };
...@@ -45,9 +45,9 @@ ...@@ -45,9 +45,9 @@
"babel-jest": "^25.1.0", "babel-jest": "^25.1.0",
"babel-plugin-module-resolver": "^4.0.0", "babel-plugin-module-resolver": "^4.0.0",
"eslint": "^6.5.1", "eslint": "^6.5.1",
"husky": "^5.1.3", "husky": "^5.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"jest-transform-stub": "^2.0.0",
"lint-staged": "^10.5.4", "lint-staged": "^10.5.4",
"metro-react-native-babel-preset": "^0.59.0", "metro-react-native-babel-preset": "^0.59.0",
"react-test-renderer": "16.13.1", "react-test-renderer": "16.13.1",
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
} }
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx}": [ "./src/*.{ts,tsx}": [
"yarn run lint", "yarn run lint",
"yarn run prettify" "yarn run prettify"
] ]
......
import React from 'react';
import { render } from '@testing-library/react-native';
import Loader from '.';
describe('Loader component', () => {
it('renders correctly', () => {
render(<Loader />);
});
});
import React from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { colors } from 'styles';
const Loader = () => (
<View style={styles.container}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
});
export default Loader;
export { default as BigButton } from './Button';
export { default as WizardContainer } from './WizardContainer';
export { default as AutoImage } from './AutoImage'; export { default as AutoImage } from './AutoImage';
export { default as ResultCard } from './ResultCard'; export { default as BigButton } from './Button';
export { default as Statistic } from './Statistic';
export { default as CarouselPagination } from './CarouselPagination'; export { default as CarouselPagination } from './CarouselPagination';
export { default as InfoCard } from './InfoCard'; export { default as InfoCard } from './InfoCard';
export { default as Loader } from './Loader';
export { default as ResultCard } from './ResultCard';
export { default as Statistic } from './Statistic';
export { default as Toast } from 'react-native-toast-message'; export { default as Toast } from 'react-native-toast-message';
export { default as WizardContainer } from './WizardContainer';
export default { export default {
dietProfileId: 'DIET_PROFILE_ID', dietProfileId: 'DIET_PROFILE_ID',
selectedProgramId: 'PROGRAM_ID',
selectedNutritionistId: 'NUTRITIONIST_ID',
}; };
import { DietelaProgram } from 'services/cart/models';
const prices = {
oneWeek: '239,900',
oneMonth: '609,900',
threeMonths: '1,659,900',
sixMonths: '3,449,000',
};
export const dietPrograms = {
[DietelaProgram.TRIAL]: {
title: 'One Time Consulation (7 Hari)',
price: prices.oneWeek,
},
[DietelaProgram.BALANCED_1]: {
title: 'Balanced Diet (1 Bulan)',
price: prices.oneMonth,
},
[DietelaProgram.BALANCED_3]: {
title: 'Balanced Diet (3 Bulan)',
price: prices.threeMonths,
},
[DietelaProgram.BALANCED_6]: {
title: 'Balanced Diet (6 Bulan)',
price: prices.sixMonths,
},
[DietelaProgram.GOALS_1]: {
title: 'Body Goals (1 Bulan)',
price: prices.oneMonth,
},
[DietelaProgram.GOALS_3]: {
title: 'Body Goals (3 Bulan)',
price: prices.threeMonths,
},
[DietelaProgram.GOALS_6]: {
title: 'Body Goals (6 Bulan)',
price: prices.sixMonths,
},
[DietelaProgram.BABY_1]: {
title: 'Body for Baby (1 Bulan)',
price: prices.oneMonth,
},
[DietelaProgram.BABY_3]: {
title: 'Body for Baby (3 Bulan)',
price: prices.threeMonths,
},
};
import * as ROUTES from 'constants/routes'; import * as ROUTES from 'constants/routes';
import { AllAccessQuestionnaire, DietelaQuizResult, InitialPage } from 'scenes'; import {
AllAccessQuestionnaire,
ChoosePlan,
ComingSoonPage,
DietelaQuizResult,
InitialPage,
} from 'scenes';
import { FC } from 'react'; import { FC } from 'react';
export interface NavRoute { export interface NavRoute {
...@@ -23,4 +29,25 @@ export const navigation: NavRoute[] = [ ...@@ -23,4 +29,25 @@ export const navigation: NavRoute[] = [
component: DietelaQuizResult, component: DietelaQuizResult,
header: 'Dietela Quiz Result', header: 'Dietela Quiz Result',
}, },
{
name: ROUTES.choosePlan,
component: ChoosePlan,
header: 'Choose Plan',
},
// COMING SOON
{
name: ROUTES.cart,
component: ComingSoonPage,
header: 'Cart',
},
{
name: ROUTES.programDetail,
component: ComingSoonPage,
header: 'Program Dietela',
},
{
name: ROUTES.nutritionistDetail,
component: ComingSoonPage,
header: 'Nutrisionis',
},
]; ];
export const initial = 'initial-page'; export const initial = 'initial-page';
export const comingSoon = '*';
const questionnaire = 'questionnaire'; const questionnaire = 'questionnaire';
export const allAccessQuestionnaire = `${questionnaire}/all-access`; export const allAccessQuestionnaire = `${questionnaire}/all-access`;
export const dietelaQuizResult = `${questionnaire}/dietela-quiz-result`; export const dietelaQuizResult = `${questionnaire}/dietela-quiz-result`;
export const cart = 'cart';
export const choosePlan = `${cart}/choose-plan`;
export const programDetail = 'dietela-program';
export const nutritionistDetail = 'nutritionist';
export { default as useApi } from './useApi';
export { default as useForm } from './useForm'; export { default as useForm } from './useForm';
import { useState, useEffect } from 'react';
import { ApiResponse, Response } from 'services/api';
import { Toast } from 'components/core';
const useApi = <T>(
fetchApi: () => ApiResponse<T>,
): {
isLoading: boolean;
} & Response<T> => {
const [isLoading, setIsLoading] = useState(false);
const [response, setResponse] = useState({
success: false,
});
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const apiResponse = await fetchApi();
setResponse(apiResponse);
if (!apiResponse.success) {
Toast.show({
type: 'error',
text1: 'Gagal memuat data',
text2: 'Terjadi kesalahan pada sisi kami.',
});
}
setIsLoading(false);
};
fetchData();
}, [fetchApi]);
return {
isLoading,
...response,
};
};
export default useApi;
import React from 'react';
import { render } from '@testing-library/react-native';
import PricingCard from '.';
describe('PricingCard component', () => {
it('renders correctly', () => {
render(<PricingCard onReadMore={jest.fn()} />);
});
it('shows Terpilih button when selected', () => {
const { getByText } = render(
<PricingCard onReadMore={jest.fn()} isSelected />,
);
expect(getByText(/Terpilih/i)).toBeTruthy();
});
it('shows price if price props is provided', () => {
const { getByText } = render(
<PricingCard onReadMore={jest.fn()} price="123456" />,
);
expect(getByText(/123456/i)).toBeTruthy();
});
it('shows info list if info props is provided', () => {
const { getByText } = render(
<PricingCard onReadMore={jest.fn()} info={['very good']} />,
);
expect(getByText(/very good/i)).toBeTruthy();
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import { Text, Button, ListItem, Icon } from 'react-native-elements';
import { colors } from 'styles';
import { Props } from './types';
import { styles } from './styles';
const PricingCard: FC<Props> = ({
title,
price,
info = [],
onButtonPress,
onReadMore,
isSelected,
}) => {
const buttonProps = isSelected
? {
buttonStyle: styles.selectedButton,
icon: {
name: 'check-circle',
size: 20,
color: 'white',
},
title: 'Terpilih',
}
: {
buttonStyle: styles.buttonStyle,
titleStyle: styles.titleStyle,
title: `Pilih ${title}`,
};
return (
<View style={[styles.container, isSelected ? styles.selected : null]}>
<Text style={styles.title}>{title}</Text>
{price ? (
<View style={styles.basePriceContainer}>
<Text style={styles.currency}>Rp</Text>
<Text style={styles.basePrice}>{price}</Text>
</View>
) : null}
<View style={styles.info}>
{info.map((item, i) => (
<ListItem key={`info${i}`} containerStyle={styles.info}>
<Icon name="check" color={colors.primary} />
<ListItem.Content>
<ListItem.Title>{item.trim()}</ListItem.Title>
</ListItem.Content>
</ListItem>
))}
</View>
<Button
onPress={onButtonPress}
containerStyle={styles.buttonContainer}
{...buttonProps}
/>
<Button
title="Baca selengkapnya"
type="clear"
icon={{
name: 'keyboard-arrow-right',
size: 25,
color: colors.primaryVariant,
}}
onPress={onReadMore}
iconRight
titleStyle={styles.readMore}
/>
</View>
);
};
export default PricingCard;
import { StyleSheet } from 'react-native';
import { colors, typography } from 'styles';
export const styles = StyleSheet.create({
container: {
borderWidth: 1,
borderColor: colors.border,
borderRadius: 6,
padding: 20,
paddingBottom: 0,
marginTop: 20,
},
selected: {
borderColor: colors.secondaryVariant,
},
title: {
...typography.headingMedium,
marginBottom: 4,
},
basePriceContainer: {
display: 'flex',
flexDirection: 'row',
marginBottom: 10,
},
currency: {
...typography.bodySmall,
color: colors.formLabel,
marginRight: 6,
marginTop: 12,
},
basePrice: {
...typography.displayMediumMontserrat,
fontSize: 40,
color: colors.primary,
},
buttonContainer: {
marginTop: 10,
},
buttonStyle: {
backgroundColor: colors.secondaryVariant,
},
selectedButton: {
backgroundColor: colors.primaryVariant,
},
titleStyle: {
color: colors.textBlack,
},
readMore: {
color: colors.primaryVariant,
},
info: {
paddingVertical: 4,
},
});
import { PricingCardProps } from 'react-native-elements';
export interface Props extends PricingCardProps {
onReadMore: () => void;
isSelected?: boolean;
}
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import PricingList from '.';
describe('PricingList component', () => {
let value: any = null;
const props = {
headerText: 'hello',
items: [
{
title: 'Choice 1',
price: '800000',
value: 'the_choice',
onReadMore: jest.fn(),
},
{
title: 'Choice 2',
price: '800000',
value: 'the_choice_2',
onReadMore: jest.fn(),
},
],
value,
onChange: (v: any) => (value = v),
};
it('only selects one pricing card at a time', () => {
const { getByText } = render(<PricingList {...props} />);
fireEvent.press(getByText(/Pilih Choice 1/i));
expect(value).toEqual('the_choice');
fireEvent.press(getByText(/Pilih Choice 2/i));
expect(value).toEqual('the_choice_2');
});
});
import React, { FC } from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-elements';
import PricingCard from '../PricingCard';
import { Props } from './types';
import { typographyStyles } from 'styles';
const PricingList: FC<Props> = ({ headerText, items, value, onChange }) => (
<View>
<Text style={typographyStyles.headingMedium}>{headerText}</Text>
{items.map((item) => (
<PricingCard
{...item}
onButtonPress={() => onChange(item.value)}
isSelected={value === item.value}
key={item.value}
/>
))}
</View>
);
export default PricingList;
export interface Props {
headerText: string;
items: {
title: string;
price?: string;
value: string | number;
info?: string[];
onReadMore: () => void;
}[];
value: any;
onChange: (_: any) => void;
onButtonPress?: () => void;
}
export { default as PricingList } from './PricingList';
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment