Skip to content

Commit 3639ef3

Browse files
authored
feat: Add Search filters for method and status code (#83)
* Add request method filter * Improve search filter * Improve modal * Filter by status * A11y Fixes * Update icons * Lint * Improve status input on android * Fix format
1 parent 0ecd3e8 commit 3639ef3

14 files changed

+573
-168
lines changed

example/src/App.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
StyleSheet,
44
Button,
55
SafeAreaView,
6-
Platform,
76
View,
87
Text,
98
TouchableOpacity,
@@ -120,7 +119,10 @@ export default function App() {
120119

121120
return (
122121
<SafeAreaView style={styles.container}>
123-
<StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} />
122+
<StatusBar
123+
barStyle={isDark ? 'light-content' : 'dark-content'}
124+
backgroundColor={isDark ? '#2d2a28' : 'white'}
125+
/>
124126
<View style={styles.header}>
125127
<TouchableOpacity
126128
style={styles.navButton}
@@ -154,7 +156,7 @@ const themedStyles = (dark = false) =>
154156
container: {
155157
flex: 1,
156158
backgroundColor: dark ? '#2d2a28' : 'white',
157-
paddingTop: Platform.OS === 'android' ? 25 : 0,
159+
paddingTop: 0,
158160
},
159161
header: {
160162
flexDirection: 'row',

src/components/AppContext.tsx

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { Dispatch, useContext, useReducer } from 'react';
2+
3+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
4+
5+
const initialFilter = {
6+
methods: new Set<Method>(),
7+
};
8+
9+
type Filter = {
10+
methods?: typeof initialFilter.methods;
11+
status?: number;
12+
statusErrors?: boolean;
13+
};
14+
15+
interface AppState {
16+
search: string;
17+
filter: Filter;
18+
filterActive: boolean;
19+
}
20+
21+
type Action =
22+
| {
23+
type: 'SET_SEARCH';
24+
payload: string;
25+
}
26+
| {
27+
type: 'SET_FILTER';
28+
payload: Filter;
29+
}
30+
| {
31+
type: 'CLEAR_FILTER';
32+
};
33+
34+
const initialState: AppState = {
35+
search: '',
36+
filter: initialFilter,
37+
filterActive: false,
38+
};
39+
40+
const AppContext = React.createContext<
41+
AppState & { dispatch: Dispatch<Action> }
42+
>({
43+
...initialState,
44+
// @ts-ignore
45+
dispatch: {},
46+
});
47+
48+
const reducer = (state: AppState, action: Action): AppState => {
49+
switch (action.type) {
50+
case 'SET_SEARCH':
51+
return {
52+
...state,
53+
search: action.payload,
54+
};
55+
case 'SET_FILTER':
56+
return {
57+
...state,
58+
filter: action.payload,
59+
filterActive:
60+
!!action.payload.methods?.size ||
61+
!!action.payload.status ||
62+
!!action.payload.statusErrors,
63+
};
64+
case 'CLEAR_FILTER':
65+
return {
66+
...state,
67+
filter: initialFilter,
68+
filterActive: false,
69+
};
70+
default:
71+
return state;
72+
}
73+
};
74+
75+
export const useAppContext = () => useContext(AppContext);
76+
export const useDispatch = () => useAppContext().dispatch;
77+
78+
export const AppContextProvider = ({
79+
children,
80+
}: {
81+
children: React.ReactNode;
82+
}) => {
83+
const [state, dispatch] = useReducer(reducer, initialState);
84+
85+
return (
86+
<AppContext.Provider value={{ ...state, dispatch }}>
87+
{children}
88+
</AppContext.Provider>
89+
);
90+
};
91+
92+
export default AppContext;

src/components/Button.tsx

+14-4
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,36 @@ import {
55
StyleSheet,
66
StyleProp,
77
ViewStyle,
8+
TextStyle,
89
} from 'react-native';
910
import { useThemedStyles, Theme } from '../theme';
1011

11-
interface Props {
12+
type Props = {
1213
children: string;
1314
fullWidth?: boolean;
1415
onPress: () => void;
1516
style?: StyleProp<ViewStyle>;
16-
}
17+
textStyle?: StyleProp<TextStyle>;
18+
} & TouchableOpacity['props'];
1719

18-
const Button: React.FC<Props> = ({ children, fullWidth, style, onPress }) => {
20+
const Button: React.FC<Props> = ({
21+
children,
22+
fullWidth,
23+
style,
24+
textStyle,
25+
onPress,
26+
...rest
27+
}) => {
1928
const styles = useThemedStyles(themedStyles);
2029

2130
return (
2231
<TouchableOpacity
2332
accessibilityRole="button"
2433
onPress={onPress}
2534
style={style}
35+
{...rest}
2636
>
27-
<Text style={[styles.button, fullWidth && styles.fullWidth]}>
37+
<Text style={[styles.button, fullWidth && styles.fullWidth, textStyle]}>
2838
{children}
2939
</Text>
3040
</TouchableOpacity>

src/components/Filters.tsx

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import React from 'react';
2+
import { View, Text, TextInput, StyleSheet } from 'react-native';
3+
import NLModal from './Modal';
4+
import Button from './Button';
5+
import { useAppContext } from './AppContext';
6+
import { Theme, useTheme, useThemedStyles } from '../theme';
7+
8+
const FilterButton = ({
9+
onPress,
10+
active,
11+
children,
12+
}: {
13+
onPress: () => void;
14+
active?: boolean;
15+
children: string;
16+
}) => {
17+
const styles = useThemedStyles(themedStyles);
18+
19+
return (
20+
<Button
21+
style={[styles.methodButton, active && styles.buttonActive]}
22+
textStyle={[styles.buttonText, active && styles.buttonActiveText]}
23+
onPress={onPress}
24+
accessibilityRole="checkbox"
25+
accessibilityState={{ checked: active }}
26+
>
27+
{children}
28+
</Button>
29+
);
30+
};
31+
32+
const Filters = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
33+
const { filter, dispatch } = useAppContext();
34+
const styles = useThemedStyles(themedStyles);
35+
const theme = useTheme();
36+
37+
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const;
38+
39+
return (
40+
<View>
41+
<NLModal visible={open} onClose={onClose} title="Filters">
42+
<Text style={styles.subTitle} accessibilityRole="header">
43+
Method
44+
</Text>
45+
<View style={styles.methods}>
46+
{methods.map((method) => (
47+
<FilterButton
48+
key={method}
49+
active={filter.methods?.has(method)}
50+
onPress={() => {
51+
const newMethods = new Set(filter.methods);
52+
if (newMethods.has(method)) {
53+
newMethods.delete(method);
54+
} else {
55+
newMethods.add(method);
56+
}
57+
58+
dispatch({
59+
type: 'SET_FILTER',
60+
payload: {
61+
...filter,
62+
methods: newMethods,
63+
},
64+
});
65+
}}
66+
>
67+
{method}
68+
</FilterButton>
69+
))}
70+
</View>
71+
<Text style={styles.subTitle} accessibilityRole="header">
72+
Status
73+
</Text>
74+
<View style={styles.methods}>
75+
<FilterButton
76+
active={filter.statusErrors}
77+
onPress={() => {
78+
dispatch({
79+
type: 'SET_FILTER',
80+
payload: {
81+
...filter,
82+
statusErrors: !filter.statusErrors,
83+
status: undefined,
84+
},
85+
});
86+
}}
87+
>
88+
Errors
89+
</FilterButton>
90+
<TextInput
91+
style={styles.statusInput}
92+
placeholder="Status Code"
93+
placeholderTextColor={theme.colors.muted}
94+
keyboardType="number-pad"
95+
value={filter.status?.toString() || ''}
96+
maxLength={3}
97+
accessibilityLabel="Status Code"
98+
onChangeText={(text) => {
99+
const status = parseInt(text, 10);
100+
dispatch({
101+
type: 'SET_FILTER',
102+
payload: {
103+
...filter,
104+
statusErrors: false,
105+
status: isNaN(status) ? undefined : status,
106+
},
107+
});
108+
}}
109+
/>
110+
</View>
111+
<View style={styles.divider} />
112+
<Button
113+
textStyle={styles.clearButton}
114+
onPress={() => {
115+
dispatch({
116+
type: 'CLEAR_FILTER',
117+
});
118+
onClose();
119+
}}
120+
>
121+
Reset All Filters
122+
</Button>
123+
</NLModal>
124+
</View>
125+
);
126+
};
127+
128+
const themedStyles = (theme: Theme) =>
129+
StyleSheet.create({
130+
subTitle: {
131+
color: theme.colors.text,
132+
fontSize: 16,
133+
fontWeight: 'bold',
134+
marginBottom: 8,
135+
},
136+
filterValue: {
137+
fontWeight: 'bold',
138+
},
139+
methods: {
140+
flexDirection: 'row',
141+
flexWrap: 'wrap',
142+
marginBottom: 10,
143+
},
144+
methodButton: {
145+
margin: 2,
146+
borderWidth: 1,
147+
borderRadius: 10,
148+
borderColor: theme.colors.secondary,
149+
},
150+
statusInput: {
151+
color: theme.colors.text,
152+
marginLeft: 10,
153+
borderColor: theme.colors.secondary,
154+
padding: 5,
155+
borderBottomWidth: 1,
156+
minWidth: 100,
157+
},
158+
buttonText: {
159+
fontSize: 12,
160+
},
161+
buttonActive: {
162+
backgroundColor: theme.colors.secondary,
163+
},
164+
buttonActiveText: {
165+
color: theme.colors.onSecondary,
166+
},
167+
clearButton: {
168+
color: theme.colors.statusBad,
169+
},
170+
divider: {
171+
height: 1,
172+
backgroundColor: theme.colors.muted,
173+
marginTop: 20,
174+
},
175+
});
176+
177+
export default Filters;

src/components/Header.tsx

+6-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import React from 'react';
2-
import {
3-
View,
4-
Text,
5-
StyleSheet,
6-
TouchableOpacity,
7-
Share,
8-
Image,
9-
} from 'react-native';
2+
import { View, Text, StyleSheet, Share } from 'react-native';
103
import { useThemedStyles, Theme } from '../theme';
4+
import Icon from './Icon';
115

126
interface Props {
137
children: string;
@@ -27,20 +21,15 @@ const Header: React.FC<Props> = ({ children, shareContent }) => {
2721
</Text>
2822

2923
{!!shareContent && (
30-
<TouchableOpacity
24+
<Icon
25+
name="share"
3126
testID="header-share"
3227
accessibilityLabel="Share"
33-
accessibilityRole="button"
3428
onPress={() => {
3529
Share.share({ message: shareContent });
3630
}}
37-
>
38-
<Image
39-
source={require('./images/share.png')}
40-
resizeMode="contain"
41-
style={styles.shareIcon}
42-
/>
43-
</TouchableOpacity>
31+
iconStyle={styles.shareIcon}
32+
/>
4433
)}
4534
</View>
4635
);
@@ -59,8 +48,6 @@ const themedStyles = (theme: Theme) =>
5948
shareIcon: {
6049
width: 24,
6150
height: 24,
62-
marginRight: 10,
63-
tintColor: theme.colors.text,
6451
},
6552
container: {
6653
justifyContent: 'space-between',

0 commit comments

Comments
 (0)