refactor: use types for props, not interfaces + update enum values + remove unnecessary useCallbacks from List.tsx

This commit is contained in:
Dawid Wysokiński 2021-11-01 17:17:38 +01:00
parent 1b673d1333
commit 66b72dcb77
Signed by: Kichiyaki
GPG Key ID: EF14026D247EB867
29 changed files with 149 additions and 139 deletions

View File

@ -5,27 +5,28 @@ import buildURL from 'utils/buildURL';
import { Linking, StyleSheet } from 'react-native';
import { ActionSheet, Icon, NativeBase } from 'native-base';
export interface MenuProps
extends Omit<NativeBase.Icon, 'onPress' | 'type' | 'name'> {}
const OPTIONS = ['Strona internetowa', 'Kontakt', 'Anuluj'];
const WEBSITE_OPT_INDEX = 0;
const CONTACT_OPT_INDEX = 1;
const CANCEL_OPT_INDEX = OPTIONS.length - 1;
enum OptionIndex {
WEBSITE,
CONTACT,
CANCEL,
}
export type MenuProps = Omit<NativeBase.Icon, 'onPress' | 'type' | 'name'>;
const Menu = ({ style, ...rest }: MenuProps) => {
const showMenu = () => {
ActionSheet.show(
{
options: OPTIONS,
cancelButtonIndex: CANCEL_OPT_INDEX,
cancelButtonIndex: OptionIndex.CANCEL,
},
index => {
switch (index) {
case WEBSITE_OPT_INDEX:
case OptionIndex.WEBSITE:
Linking.openURL(WEBSITE);
break;
case CONTACT_OPT_INDEX:
case OptionIndex.CONTACT:
Linking.openURL(buildURL('email', EMAIL));
break;
}

View File

@ -1,7 +1,7 @@
export enum Event {
SaveQualification = 'save_qualification',
UnSaveQualification = 'unsave_qualification',
StartTest = 'start_test',
FinishTest = 'finish_test',
SelectAnswer = 'select_answer',
SAVE_QUALIFICATION = 'save_qualification',
UNSAVE_QUALIFICATION = 'unsave_qualification',
START_TEST = 'start_test',
FINISH_TEST = 'finish_test',
SELECT_ANSWER = 'select_answer',
}

View File

@ -1,6 +1,6 @@
export enum Screen {
Home = 'HomeScreen',
Test = 'TestScreen',
HOME = 'HomeScreen',
TEST = 'TestScreen',
}
export type TestScreenParams = {
@ -9,6 +9,6 @@ export type TestScreenParams = {
};
export type AppStackParamList = {
[Screen.Home]: undefined;
[Screen.Test]: TestScreenParams;
[Screen.HOME]: undefined;
[Screen.TEST]: TestScreenParams;
};

View File

@ -15,17 +15,20 @@ export const createClient = (
cache: new InMemoryCache(),
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (__DEV__) {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
if (!__DEV__) {
return;
}
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
}),
new HttpLink({

View File

@ -66,7 +66,7 @@ export default (variables /* : * */ = variable) => {
},
},
backgroundColor:
Platform.OS === OS.Android ? variables.footerDefaultBg : undefined,
Platform.OS === OS.ANDROID ? variables.footerDefaultBg : undefined,
flexDirection: 'row',
justifyContent: 'space-between',
flex: 1,

View File

@ -47,9 +47,9 @@ export default (variables /* : * */ = variable) => {
shadowOffset: null,
shadowRadius: null,
shadowOpacity: null,
paddingTop: platform === OS.Android ? StatusBar.currentHeight : undefined,
paddingTop: platform === OS.ANDROID ? StatusBar.currentHeight : undefined,
height:
platform === OS.Android
platform === OS.ANDROID
? variables.toolbarHeight + (StatusBar.currentHeight ?? 0)
: variables.toolbarHeight,
},

View File

@ -97,7 +97,7 @@ export default (variables /* : * */ = variable) => {
paddingTop:
platform === OS.IOS ? variables.listItemPadding + 25 : undefined,
paddingBottom:
platform === OS.Android ? variables.listItemPadding + 20 : undefined,
platform === OS.ANDROID ? variables.listItemPadding + 20 : undefined,
flexDirection: 'row',
borderColor: variables.listBorderColor,
'NativeBase.Text': {

View File

@ -12,8 +12,8 @@ export default (variables /* : * */ = variable) => {
justifyContent: 'center',
'.scrollable': {
paddingHorizontal: 20,
flex: platform === OS.Android ? 0 : 1,
minWidth: platform === OS.Android ? undefined : 60,
flex: platform === OS.ANDROID ? 0 : 1,
minWidth: platform === OS.ANDROID ? undefined : 60,
},
'NativeBase.Text': {
color: variables.topTabBarTextColor,

View File

@ -211,6 +211,6 @@ export type Variables = {
};
export enum OS {
Android = 'android',
ANDROID = 'android',
IOS = 'ios',
}

View File

@ -1,19 +1,15 @@
import React, { useCallback, useState } from 'react';
import React, { PropsWithChildren, useCallback, useState } from 'react';
import { useEffectOnce, useUpdateEffect } from 'react-use';
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
import analytics from '@react-native-firebase/analytics';
import { context as Context } from './context';
import { Event } from 'config/analytics';
export interface SavedQualificationsProviderProps {
children?: React.ReactNode;
}
const ASYNC_STORAGE_KEY = 'saved_qualifications';
export const SavedQualificationsProvider = ({
children,
}: SavedQualificationsProviderProps) => {
}: PropsWithChildren<{}>) => {
const [loading, setLoading] = useState(true);
const [savedQualifications, setSavedQualifications] = useState<number[]>([]);
const asyncStorage = useAsyncStorage(ASYNC_STORAGE_KEY);
@ -54,7 +50,7 @@ export const SavedQualificationsProvider = ({
);
analytics().logEvent(
save ? Event.SaveQualification : Event.UnSaveQualification,
save ? Event.SAVE_QUALIFICATION : Event.UNSAVE_QUALIFICATION,
{
id: id.toString(),
},

View File

@ -1,5 +1,5 @@
import 'react-native-gesture-handler';
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import { ApolloProvider } from '@apollo/client';
import RNBootSplash from 'react-native-bootsplash';
import { Root, StyleProvider } from 'native-base';
@ -18,8 +18,12 @@ const BaseApp = () => {
};
const App = () => {
const theme = useRef(createTheme(variables)).current;
const client = useRef(createClient(API_URI)).current;
const theme = useMemo(() => {
return createTheme(variables);
}, []);
const client = useMemo(() => {
return createClient(API_URI);
}, []);
return (
<ApolloProvider client={client}>

View File

@ -7,7 +7,7 @@ import Professions from './components/Professions/Professions';
const HomeScreen = () => {
const [search, setSearch] = useState('');
const [mode, setMode] = useState(Mode.All);
const [mode, setMode] = useState(Mode.ALL);
return (
<Container>

View File

@ -5,9 +5,9 @@ import { Keyboard, StyleSheet, TextInput } from 'react-native';
import { Icon, Input, Item, Header as NBHeader, View } from 'native-base';
import Menu from 'common/Menu/Menu';
export interface HeaderProps {
export type HeaderProps = {
onSearch?: (search: string) => void;
}
};
const Header = ({ onSearch }: HeaderProps) => {
const inputRef = useRef<Input>(null);

View File

@ -2,28 +2,28 @@ import React from 'react';
import { Button, Segment, Text } from 'native-base';
export enum Mode {
All,
Saved,
ALL,
SAVED,
}
export interface ModeSelectorProps {
export type ModeSelectorProps = {
mode: Mode;
onChangeMode: (mode: Mode) => void;
}
};
const ModeSelector = ({ mode, onChangeMode }: ModeSelectorProps) => {
return (
<Segment>
<Button
first
onPress={() => onChangeMode(Mode.All)}
active={mode === Mode.All}
onPress={() => onChangeMode(Mode.ALL)}
active={mode === Mode.ALL}
>
<Text allowFontScaling={false}>Wszystkie</Text>
</Button>
<Button
onPress={() => onChangeMode(Mode.Saved)}
active={mode === Mode.Saved}
onPress={() => onChangeMode(Mode.SAVED)}
active={mode === Mode.SAVED}
>
<Text allowFontScaling={false}>Zapisane</Text>
</Button>

View File

@ -1,4 +1,4 @@
import React, { forwardRef, useCallback } from 'react';
import React, { forwardRef } from 'react';
import {
FlatList,
@ -11,24 +11,20 @@ import ListEmpty from './ListEmpty';
import ListLoading from './ListLoading';
export type Item = ListItemProps;
export interface ListProps
extends Pick<
FlatListProps<Item>,
'refreshing' | 'onRefresh' | 'contentContainerStyle'
> {
export type ListProps = {
items: Item[];
loading?: boolean;
}
} & Pick<
FlatListProps<Item>,
'refreshing' | 'onRefresh' | 'contentContainerStyle'
>;
const noop = () => {};
const keyExtractor = (item: ListItemProps) => item.id!;
const renderItem = ({ item }: { item: Item }) => <ListItem {...item} />;
const MyList = forwardRef<FlatList<Item>, ListProps>(
const List = forwardRef<FlatList<Item>, ListProps>(
({ items, refreshing, onRefresh, loading, ...rest }: ListProps, ref) => {
const renderItem = useCallback(({ item }: { item: Item }) => {
return <ListItem {...item} />;
}, []);
const keyExtractor = useCallback(item => item.id, []);
return (
<FlatList
data={items}
@ -61,4 +57,4 @@ const styles = StyleSheet.create({
},
});
export default MyList;
export default List;

View File

@ -1,12 +1,11 @@
import React, { Fragment, useMemo, memo } from 'react';
import { Icon, Left, ListItem, NativeBase, Right, Text } from 'native-base';
export interface ListItemProps
extends Pick<NativeBase.ListItem, 'itemDivider' | 'itemHeader'> {
export type ListItemProps = {
onPress?: (id: string) => void;
id?: string;
text: string;
}
} & Pick<NativeBase.ListItem, 'itemDivider' | 'itemHeader'>;
const MyListItem = ({
onPress,

View File

@ -4,25 +4,27 @@ import { Alert, Linking } from 'react-native';
import buildURL from 'utils/buildURL';
import { EMAIL } from 'config/app';
export interface NetworkConnectionAlertProps {
export type NetworkConnectionAlertProps = {
error?: ApolloError;
}
};
const NetworkConnectionAlert = ({ error }: NetworkConnectionAlertProps) => {
useUpdateEffect(() => {
if (error && error.networkError) {
Alert.alert(
'Problem z połączeniem',
'Prosimy o sprawdzenie połączenia z internetem / spróbowanie ponownie później. Przepraszamy za utrudnienia.',
[
{
text: 'Zgłoś problem',
onPress: () => Linking.openURL(buildURL('email', EMAIL)),
},
{ text: 'OK' },
],
);
if (!error || !error.networkError) {
return;
}
Alert.alert(
'Problem z połączeniem',
'Prosimy o sprawdzenie połączenia z internetem / spróbowanie ponownie później. Przepraszamy za utrudnienia.',
[
{
text: 'Zgłoś problem',
onPress: () => Linking.openURL(buildURL('email', EMAIL)),
},
{ text: 'OK' },
],
);
}, [error]);
return null;

View File

@ -11,10 +11,10 @@ import List, { Item } from './List/List';
import QualificationModal from './QualificationModal';
import NetworkConnectionAlert from './NetworkConnectionAlert';
export interface ProfessionsProps {
export type ProfessionsProps = {
mode: Mode;
search: string;
}
};
const ID_SEPARATOR = '.';
const getQualificationAndProfessionID = (str: string): [number, number] => {
@ -44,16 +44,21 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
const [professionID, qualificationID] = getQualificationAndProfessionID(
id,
);
const profession = professions.find(p => p.id === professionID);
if (profession) {
const qualification = profession.qualifications.find(
q => q.id === qualificationID,
);
if (qualification) {
setSelectedQualification(qualification);
setIsModalVisible(true);
}
if (!profession) {
return;
}
const qualification = profession.qualifications.find(
q => q.id === qualificationID,
);
if (!qualification) {
return;
}
setSelectedQualification(qualification);
setIsModalVisible(true);
},
[setIsModalVisible, setSelectedQualification, professions],
);
@ -62,7 +67,9 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
if (professionsLoading) {
return;
}
let items: Item[] = [];
professions.forEach(profession => {
const qualifications = profession.qualifications
.filter(
@ -70,7 +77,7 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
(!search ||
qualification.name.toLowerCase().includes(search) ||
qualification.code.toLowerCase().includes(search)) &&
(mode === Mode.All || isSaved(qualification.id)),
(mode === Mode.ALL || isSaved(qualification.id)),
)
.map(
(qualification): Item => {
@ -83,18 +90,21 @@ const Professions = ({ mode, search }: ProfessionsProps) => {
};
},
);
if (qualifications.length > 0) {
items = [
...items,
{
text: profession.name,
itemHeader: true,
itemDivider: true,
id: 'P' + profession.id,
} as Item,
...qualifications,
];
if (qualifications.length === 0) {
return;
}
items = [
...items,
{
text: profession.name,
itemHeader: true,
itemDivider: true,
id: 'P' + profession.id,
} as Item,
...qualifications,
];
});
setListItems(items);

View File

@ -19,10 +19,9 @@ import {
} from 'native-base';
import Modal, { ModalProps } from 'common/Modal/Modal';
export interface QualificationModalProps
extends Pick<ModalProps, 'visible' | 'onPressBackdrop'> {
export type QualificationModalProps = {
qualification: Maybe<Qualification>;
}
} & Pick<ModalProps, 'visible' | 'onPressBackdrop'>;
const QualificationModal = ({
qualification,
@ -81,7 +80,7 @@ const QualificationModal = ({
if (onPressBackdrop) {
onPressBackdrop();
}
navigation.navigate(Screen.Test, {
navigation.navigate(Screen.TEST, {
qualificationID: qualification?.id ?? 0,
limit: question,
});

View File

@ -15,8 +15,8 @@ const AppStack = createStackNavigator<AppStackParamList>();
const AppScreens = () => (
<AppStack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name={Screen.Home} component={HomeScreen} />
<Stack.Screen name={Screen.Test} component={TestScreen} />
<Stack.Screen name={Screen.HOME} component={HomeScreen} />
<Stack.Screen name={Screen.TEST} component={TestScreen} />
</AppStack.Navigator>
);

View File

@ -15,9 +15,9 @@ import Suggestions from './components/Suggestions/Suggestions';
import Test from './components/Test/Test';
import Content from './components/Content/Content';
export interface TestScreenProps {
route: RouteProp<AppStackParamList, Screen.Test>;
}
export type TestScreenProps = {
route: RouteProp<AppStackParamList, Screen.TEST>;
};
type QueryGenerateTestSimilarQualificationsQualificationArgs = {
limitTest: Scalars['Int'];

View File

@ -2,10 +2,10 @@ import React from 'react';
import { StyleSheet } from 'react-native';
import { Content as ContentNB, NativeBase } from 'native-base';
export interface ContentProps {
export type ContentProps = {
children?: React.ReactNode;
contentContainerStyle?: NativeBase.Content['contentContainerStyle'];
}
};
const Content = ({ children, contentContainerStyle }: ContentProps) => {
return (

View File

@ -12,9 +12,9 @@ import {
} from 'native-base';
import Menu from 'common/Menu/Menu';
export interface HeaderProps {
export type HeaderProps = {
title: string;
}
};
const Header = ({ title }: HeaderProps) => {
const navigation = useNavigation();

View File

@ -9,9 +9,9 @@ import { StyleSheet } from 'react-native';
import { H1, View, H3, Card, CardItem, Text, Button, Body } from 'native-base';
import Content from '../Content/Content';
export interface SuggestionsProps {
export type SuggestionsProps = {
qualifications: Qualification[];
}
};
const Suggestions = ({ qualifications }: SuggestionsProps) => {
const navigation = useNavigation();
@ -51,7 +51,7 @@ const Suggestions = ({ qualifications }: SuggestionsProps) => {
<Button
key={question}
onPress={() => {
navigation.navigate(Screen.Test, {
navigation.navigate(Screen.TEST, {
qualificationID: qualification.id,
limit: question,
});

View File

@ -9,12 +9,12 @@ export enum AlertVariant {
Warning = 'warning',
}
export interface AlertProps extends Pick<NativeBase.View, 'style'> {
export type AlertProps = {
variant?: AlertVariant;
title: React.ReactNode;
description?: React.ReactNode;
actions?: React.ReactNode;
}
} & Pick<NativeBase.View, 'style'>;
const Alert = ({
variant = AlertVariant.Info,

View File

@ -9,9 +9,9 @@ import {
} from 'react-native';
import { H3 } from 'native-base';
export interface ImageProps extends Pick<RNImageProps, 'style'> {
export type ImageProps = {
path: string;
}
} & Pick<RNImageProps, 'style'>;
const MyImage = ({ path, style }: ImageProps) => {
const [loading, setLoading] = useState(true);

View File

@ -10,12 +10,12 @@ import Content from '../Content/Content';
import Image from './Image';
import Alert, { AlertVariant } from './Alert';
export interface QuestionProps {
export type QuestionProps = {
question: QuestionT;
reviewMode: boolean;
selectedAnswer: Answer;
selectAnswer: (a: Answer) => void;
}
};
const ANSWERS = Object.values(Answer);

View File

@ -8,14 +8,14 @@ import { Button, H1, H3, Text, View } from 'native-base';
import Content from '../Content/Content';
import Alert from './Alert';
export interface SummaryTabProps {
export type SummaryTabProps = {
reviewMode: boolean;
answers: Answer[];
questions: Question[];
finishTest: () => void;
resetTest: () => void;
qualificationID: number;
}
};
const SummaryTab = ({
reviewMode,

View File

@ -7,11 +7,11 @@ import { ScrollableTab, Tab, Tabs } from 'native-base';
import Question from './Question';
import SummaryTab from './SummaryTab';
export interface TestProps {
export type TestProps = {
questions: QuestionT[];
onReset: () => void;
qualification: Qualification;
}
};
const Test = ({ questions, onReset, qualification }: TestProps) => {
const [reviewMode, setReviewMode] = useState(false);
@ -28,7 +28,7 @@ const Test = ({ questions, onReset, qualification }: TestProps) => {
);
useEffect(() => {
analytics().logEvent(Event.StartTest, analyticsParams);
analytics().logEvent(Event.START_TEST, analyticsParams);
}, [analyticsParams]);
const handleSelectAnswer = (index: number, answer: Answer) => {
@ -40,7 +40,7 @@ const Test = ({ questions, onReset, qualification }: TestProps) => {
index === index2 ? answer : otherAnswer,
),
);
analytics().logEvent(Event.SelectAnswer, {
analytics().logEvent(Event.SELECT_ANSWER, {
qualificationID: analyticsParams.qualificationID,
questionID: questions[index].id.toString(),
answer,
@ -50,7 +50,7 @@ const Test = ({ questions, onReset, qualification }: TestProps) => {
const handleFinishTest = () => {
setReviewMode(true);
analytics().logEvent(Event.FinishTest, analyticsParams);
analytics().logEvent(Event.FINISH_TEST, analyticsParams);
};
return (