Skip to content

Commit

Permalink
feat: Add Search filters for method and status code (#83)
Browse files Browse the repository at this point in the history
* Add request method filter

* Improve search filter

* Improve modal

* Filter by status

* A11y Fixes

* Update icons

* Lint

* Improve status input on android

* Fix format
  • Loading branch information
alexbrazier authored Sep 24, 2024
1 parent 0ecd3e8 commit 3639ef3
Show file tree
Hide file tree
Showing 14 changed files with 573 additions and 168 deletions.
8 changes: 5 additions & 3 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
StyleSheet,
Button,
SafeAreaView,
Platform,
View,
Text,
TouchableOpacity,
Expand Down Expand Up @@ -120,7 +119,10 @@ export default function App() {

return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} />
<StatusBar
barStyle={isDark ? 'light-content' : 'dark-content'}
backgroundColor={isDark ? '#2d2a28' : 'white'}
/>
<View style={styles.header}>
<TouchableOpacity
style={styles.navButton}
Expand Down Expand Up @@ -154,7 +156,7 @@ const themedStyles = (dark = false) =>
container: {
flex: 1,
backgroundColor: dark ? '#2d2a28' : 'white',
paddingTop: Platform.OS === 'android' ? 25 : 0,
paddingTop: 0,
},
header: {
flexDirection: 'row',
Expand Down
92 changes: 92 additions & 0 deletions src/components/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { Dispatch, useContext, useReducer } from 'react';

type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';

const initialFilter = {
methods: new Set<Method>(),
};

type Filter = {
methods?: typeof initialFilter.methods;
status?: number;
statusErrors?: boolean;
};

interface AppState {
search: string;
filter: Filter;
filterActive: boolean;
}

type Action =
| {
type: 'SET_SEARCH';
payload: string;
}
| {
type: 'SET_FILTER';
payload: Filter;
}
| {
type: 'CLEAR_FILTER';
};

const initialState: AppState = {
search: '',
filter: initialFilter,
filterActive: false,
};

const AppContext = React.createContext<
AppState & { dispatch: Dispatch<Action> }
>({
...initialState,
// @ts-ignore
dispatch: {},
});

const reducer = (state: AppState, action: Action): AppState => {
switch (action.type) {
case 'SET_SEARCH':
return {
...state,
search: action.payload,
};
case 'SET_FILTER':
return {
...state,
filter: action.payload,
filterActive:
!!action.payload.methods?.size ||
!!action.payload.status ||
!!action.payload.statusErrors,
};
case 'CLEAR_FILTER':
return {
...state,
filter: initialFilter,
filterActive: false,
};
default:
return state;
}
};

export const useAppContext = () => useContext(AppContext);
export const useDispatch = () => useAppContext().dispatch;

export const AppContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<AppContext.Provider value={{ ...state, dispatch }}>
{children}
</AppContext.Provider>
);
};

export default AppContext;
18 changes: 14 additions & 4 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,36 @@ import {
StyleSheet,
StyleProp,
ViewStyle,
TextStyle,
} from 'react-native';
import { useThemedStyles, Theme } from '../theme';

interface Props {
type Props = {
children: string;
fullWidth?: boolean;
onPress: () => void;
style?: StyleProp<ViewStyle>;
}
textStyle?: StyleProp<TextStyle>;
} & TouchableOpacity['props'];

const Button: React.FC<Props> = ({ children, fullWidth, style, onPress }) => {
const Button: React.FC<Props> = ({
children,
fullWidth,
style,
textStyle,
onPress,
...rest
}) => {
const styles = useThemedStyles(themedStyles);

return (
<TouchableOpacity
accessibilityRole="button"
onPress={onPress}
style={style}
{...rest}
>
<Text style={[styles.button, fullWidth && styles.fullWidth]}>
<Text style={[styles.button, fullWidth && styles.fullWidth, textStyle]}>
{children}
</Text>
</TouchableOpacity>
Expand Down
177 changes: 177 additions & 0 deletions src/components/Filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';
import NLModal from './Modal';
import Button from './Button';
import { useAppContext } from './AppContext';
import { Theme, useTheme, useThemedStyles } from '../theme';

const FilterButton = ({
onPress,
active,
children,
}: {
onPress: () => void;
active?: boolean;
children: string;
}) => {
const styles = useThemedStyles(themedStyles);

return (
<Button
style={[styles.methodButton, active && styles.buttonActive]}
textStyle={[styles.buttonText, active && styles.buttonActiveText]}
onPress={onPress}
accessibilityRole="checkbox"
accessibilityState={{ checked: active }}
>
{children}
</Button>
);
};

const Filters = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
const { filter, dispatch } = useAppContext();
const styles = useThemedStyles(themedStyles);
const theme = useTheme();

const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const;

return (
<View>
<NLModal visible={open} onClose={onClose} title="Filters">
<Text style={styles.subTitle} accessibilityRole="header">
Method
</Text>
<View style={styles.methods}>
{methods.map((method) => (
<FilterButton
key={method}
active={filter.methods?.has(method)}
onPress={() => {
const newMethods = new Set(filter.methods);
if (newMethods.has(method)) {
newMethods.delete(method);
} else {
newMethods.add(method);
}

dispatch({
type: 'SET_FILTER',
payload: {
...filter,
methods: newMethods,
},
});
}}
>
{method}
</FilterButton>
))}
</View>
<Text style={styles.subTitle} accessibilityRole="header">
Status
</Text>
<View style={styles.methods}>
<FilterButton
active={filter.statusErrors}
onPress={() => {
dispatch({
type: 'SET_FILTER',
payload: {
...filter,
statusErrors: !filter.statusErrors,
status: undefined,
},
});
}}
>
Errors
</FilterButton>
<TextInput
style={styles.statusInput}
placeholder="Status Code"
placeholderTextColor={theme.colors.muted}
keyboardType="number-pad"
value={filter.status?.toString() || ''}
maxLength={3}
accessibilityLabel="Status Code"
onChangeText={(text) => {
const status = parseInt(text, 10);
dispatch({
type: 'SET_FILTER',
payload: {
...filter,
statusErrors: false,
status: isNaN(status) ? undefined : status,
},
});
}}
/>
</View>
<View style={styles.divider} />
<Button
textStyle={styles.clearButton}
onPress={() => {
dispatch({
type: 'CLEAR_FILTER',
});
onClose();
}}
>
Reset All Filters
</Button>
</NLModal>
</View>
);
};

const themedStyles = (theme: Theme) =>
StyleSheet.create({
subTitle: {
color: theme.colors.text,
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
},
filterValue: {
fontWeight: 'bold',
},
methods: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 10,
},
methodButton: {
margin: 2,
borderWidth: 1,
borderRadius: 10,
borderColor: theme.colors.secondary,
},
statusInput: {
color: theme.colors.text,
marginLeft: 10,
borderColor: theme.colors.secondary,
padding: 5,
borderBottomWidth: 1,
minWidth: 100,
},
buttonText: {
fontSize: 12,
},
buttonActive: {
backgroundColor: theme.colors.secondary,
},
buttonActiveText: {
color: theme.colors.onSecondary,
},
clearButton: {
color: theme.colors.statusBad,
},
divider: {
height: 1,
backgroundColor: theme.colors.muted,
marginTop: 20,
},
});

export default Filters;
25 changes: 6 additions & 19 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Share,
Image,
} from 'react-native';
import { View, Text, StyleSheet, Share } from 'react-native';
import { useThemedStyles, Theme } from '../theme';
import Icon from './Icon';

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

{!!shareContent && (
<TouchableOpacity
<Icon
name="share"
testID="header-share"
accessibilityLabel="Share"
accessibilityRole="button"
onPress={() => {
Share.share({ message: shareContent });
}}
>
<Image
source={require('./images/share.png')}
resizeMode="contain"
style={styles.shareIcon}
/>
</TouchableOpacity>
iconStyle={styles.shareIcon}
/>
)}
</View>
);
Expand All @@ -59,8 +48,6 @@ const themedStyles = (theme: Theme) =>
shareIcon: {
width: 24,
height: 24,
marginRight: 10,
tintColor: theme.colors.text,
},
container: {
justifyContent: 'space-between',
Expand Down
Loading

0 comments on commit 3639ef3

Please sign in to comment.