Skip to content

Commit

Permalink
feat: initial bookmarks UI (shabados#126)
Browse files Browse the repository at this point in the history
* feat: initial bookmarks implementation
* refactor: restrucutre bookmarks component inside a dir
* fix: update bookmark item types
* test: bookmark item
* fix: use recursive types for folders
* feat: use a flatlist for bookmarks list
* fix: update dummy data to follow TS type
* fix: type issues
* style: bookmark item
* test: bookmark utils
* test: bookmarks list
* fix: link bookmarks button with screen
* fix: navigation types
* style: change bookmark list item color
* refactor: restructure bookmarks
* feat: add navbar component
* feat: attempt to wrap in navigator
* feat: use brighter color to debug
* build: commit pod lock
* fix: right nav bar button press event
* fix: update colors
* fix: circular dep issue
* refactor: navigator wrapper for tests
* test: bookmark list
* refactor: remove unused imports
* style: add white space
* refactor: reduce indentation level

Co-Authored-By: Akal-Ustat Singh <akalustat.singh@gmail.com>
Co-authored-by: Saihajpreet Singh <saihajpreet.singh@gmail.com>
Co-authored-by: Harjot Singh <harjot@harkul.com>
  • Loading branch information
3 people committed Aug 2, 2021
1 parent 516acc2 commit 68e2aff
Show file tree
Hide file tree
Showing 20 changed files with 395 additions and 22 deletions.
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Expand Up @@ -408,4 +408,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 6e9396fa3f617dd06d3083a586de30fc1336a5cf

COCOAPODS: 1.10.0
COCOAPODS: 1.10.1
3 changes: 2 additions & 1 deletion src/App.tsx
Expand Up @@ -5,9 +5,10 @@ import { createStackNavigator } from '@react-navigation/stack'
import withContexts from './components/with-contexts'
import { searchScreen } from './screens/Search'
import { gurbaniScreen } from './screens/Gurbani'
import { bookmarksScreen } from './screens/Bookmarks'
import { AppStackParams } from './lib/screens'

const screens = [ gurbaniScreen, searchScreen ]
const screens = [ gurbaniScreen, searchScreen, bookmarksScreen ]

const { Screen, Navigator } = createStackNavigator<AppStackParams>()

Expand Down
18 changes: 18 additions & 0 deletions src/components/Bookmarks/Item.spec.tsx
@@ -0,0 +1,18 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'

import BookmarkItem from './Item'

describe( '<BookmarkItem />', () => {
it( 'given a press, should fire onPress with details of the bookmark item', () => {
const onPress = jest.fn()

const { getByText } = render(
<BookmarkItem title="Test Me" isFolder={false} onPress={onPress} />,
)

fireEvent.press( getByText( 'Test Me' ) )

expect( onPress ).toHaveBeenCalled()
} )
} )
40 changes: 40 additions & 0 deletions src/components/Bookmarks/Item.tsx
@@ -0,0 +1,40 @@
import React from 'react'
import { Pressable, PressableProps, StyleSheet, Text } from 'react-native'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'

import Colors from '../../themes/colors'
import { py } from '../../themes/utils'

const styles = StyleSheet.create( {
chevron: {
color: Colors.PrimaryText,
},
container: {
...py( 15 ),
paddingLeft: 10,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: Colors.Disabled,
},
title: {
color: Colors.PrimaryText,
fontSize: 18,
},
} )

type BookmarkListItemProps = {
title: string,
isFolder: boolean,
onPress: () => void,
} & PressableProps

const BookmarkListItem = ( { title, isFolder, onPress, ...props }: BookmarkListItemProps ) => (
<Pressable style={styles.container} onPress={onPress} {...props}>
<Text style={styles.title}>{title}</Text>
{isFolder && <Icon style={styles.chevron} name="chevron-right" size={25} />}
</Pressable>
)

export default BookmarkListItem
44 changes: 44 additions & 0 deletions src/components/Bookmarks/List.spec.tsx
@@ -0,0 +1,44 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react-native'

import Wrapper, { WrapperProps } from '../../lib/NavigatorContext'
import defaultFolderData from '../../defaults/collections.json'
import Screens from '../../lib/screens'

import BookmarksList from './List'

const bookmarkScreen = ( { children }: WrapperProps ) => (
<Wrapper name={Screens.Bookmarks} initialParams={{ folderData: defaultFolderData }}>
{children}
</Wrapper>
)

describe( '<BookmarksList />', () => {
it( 'should render list of bookmark items', async () => {
const { queryByText, unmount } = render(
<BookmarksList />, { wrapper: bookmarkScreen },
)

defaultFolderData.forEach( ( { name } ) => {
expect( queryByText( name ) ).toBeTruthy()
} )

unmount()
} )

it( 'should open a folder', async () => {
const { queryByText, unmount } = render(
<BookmarksList />, { wrapper: bookmarkScreen },
)

const folder = queryByText( defaultFolderData[ 0 ].name )
expect( folder ).toBeTruthy()
fireEvent.press( folder! )

defaultFolderData[ 0 ].bookmarks.forEach( ( { name } ) => {
expect( queryByText( name ) ).toBeTruthy()
} )

unmount()
} )
} )
62 changes: 62 additions & 0 deletions src/components/Bookmarks/List.tsx
@@ -0,0 +1,62 @@
import React from 'react'
import { FlatList, View, Alert, StyleSheet } from 'react-native'
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'

import Screens from '../../lib/screens'
import { NavigationParams } from '../../types/navigation'
import Colors from '../../themes/colors'

import { checkIsFolder } from './utils'
import Item from './Item'

type Route = RouteProp<NavigationParams, Screens.Bookmarks>
type Navigation = StackNavigationProp<NavigationParams, Screens.Bookmarks>

const styles = StyleSheet.create( {
screen: {
flex: 1,
backgroundColor: Colors.ModalSheet,
},
} )

const BookmarksList = () => {
const route = useRoute<Route>()
const navigation = useNavigation<Navigation>()

const { folderData } = route.params

const onItemPress = ( isFolder: boolean, name: string ) => {
if ( !isFolder ) {
return Alert.alert( `you clicked on ${name}` )
}

return navigation.push( Screens.Bookmarks, {
folderData:
folderData.find( ( folder ) => folder.name === name )?.bookmarks
|| folderData,
currentFolder: name,
} )
}

return (
<View style={styles.screen}>
<FlatList
keyExtractor={( { name } ) => name}
data={folderData}
renderItem={( { item } ) => {
const isFolder = checkIsFolder( item )
return (
<Item
title={item.name}
isFolder={isFolder}
onPress={() => onItemPress( isFolder, item.name )}
/>
)
}}
/>
</View>
)
}

export default BookmarksList
3 changes: 3 additions & 0 deletions src/components/Bookmarks/index.ts
@@ -0,0 +1,3 @@
export { default as BookmarksList } from './List'
export { default as BookmarksItem } from './Item'
export * from './types'
4 changes: 4 additions & 0 deletions src/components/Bookmarks/types.ts
@@ -0,0 +1,4 @@
export type Folder = {
name: string,
bookmarks?: Folder[],
}
18 changes: 18 additions & 0 deletions src/components/Bookmarks/utils.spec.tsx
@@ -0,0 +1,18 @@
import { checkIsFolder } from './utils'

describe( 'Bookmarks Utilities', () => {
it( 'should return false when bookmarks array is not given', () => {
expect( checkIsFolder( { name: 'test item' } ) ).toBe( false )
} )

it( 'should return true when bookmarks array is given', () => {
expect( checkIsFolder( {
name: 'test item',
bookmarks: [
{
name: 'I am an item inside folder',
},
],
} ) ).toBe( true )
} )
} )
3 changes: 3 additions & 0 deletions src/components/Bookmarks/utils.ts
@@ -0,0 +1,3 @@
import { Folder } from './types'

export const checkIsFolder = ( item: Folder ): item is Folder => !!( item ).bookmarks
12 changes: 10 additions & 2 deletions src/components/Navbar.tsx
Expand Up @@ -14,6 +14,10 @@ const styles = StyleSheet.create( {
justifyContent: 'space-between',
height: 50,
},
// this ensures these buttons are always clickable when they are visible
liftUp: {
zIndex: 10,
},
main: {
position: 'absolute',
flexDirection: 'row',
Expand Down Expand Up @@ -56,13 +60,17 @@ const Navbar = ( {
<SafeAreaView edges={[ 'left', 'top', 'right' ]} />

<View style={styles.header}>
{left}
<View style={styles.liftUp}>
{left}
</View>

<View style={styles.main}>
{main}
</View>

{right}
<View style={styles.liftUp}>
{right}
</View>
</View>
</View>
)
Expand Down
41 changes: 41 additions & 0 deletions src/defaults/collections.json
@@ -0,0 +1,41 @@
[
{
"name": "Nitnem",
"bookmarks": [
{ "name": "Jap Ji Sahib" },
{ "name": "Jaap Sahib" },
{ "name": "Tav Prasad Savaiye (Sravag Sudh)" },
{ "name": "Benti Chaupai Sahib" },
{ "name": "Anand Sahib" },
{ "name": "Rehras Sahib" },
{ "name": "Ardaas" },
{ "name": "Sohila Sahib" }
]
},
{
"name": "Sundar Gutka",
"bookmarks": [
{ "name": "Jap Ji Sahib" },
{ "name": "Jaap Sahib" },
{ "name": "Tav Prasad Savaiye (Sravag Sudh)" },
{ "name": "Benti Chaupai Sahib" },
{ "name": "Anand Sahib" },
{ "name": "Shabad Hazare" },
{ "name": "Barah Maha" },
{ "name": "Shabad Hazare Patshahi 10" },
{ "name": "Tav Prasad Savaiye (Deenan Ki)" },
{ "name": "Rehras Sahib" },
{ "name": "Ardaas" },
{ "name": "Aarti" },
{ "name": "Sohila Sahib" },
{ "name": "Bavan Akhri" },
{ "name": "Sukhmani Sahib" },
{ "name": "Asa Ki Var" },
{ "name": "Oankaar" },
{ "name": "Sidh Gosht" },
{ "name": "Lavan (Anand Karaj)" },
{ "name": "Chandi Di Var" },
{ "name": "Sri Guru Granth Sahib Paath Bhog (Ragmala)" }
]
}
]
23 changes: 23 additions & 0 deletions src/lib/NavigatorContext.tsx
@@ -0,0 +1,23 @@
import React, { ReactNode } from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'

import withContexts from '../components/with-contexts'

const { Screen, Navigator } = createStackNavigator()

export type WrapperProps = {
children: ReactNode,
initialParams?: Record<string, any>,
name?:string,
}

const Wrapper = ( { children, initialParams, name = 'default' }: WrapperProps ) => withContexts(
<NavigationContainer>
<Navigator>
<Screen name={name} initialParams={initialParams}>{() => children}</Screen>
</Navigator>
</NavigationContainer>,
)

export default Wrapper
32 changes: 32 additions & 0 deletions src/screens/Bookmarks/Navbar.spec.tsx
@@ -0,0 +1,32 @@
import { render } from '@testing-library/react-native'
import React from 'react'

import wrapper from '../../lib/NavigatorContext'

import Navbar from './Navbar'

describe( '<Navbar />', () => {
it( 'should render a back button', () => {
const { queryByTestId, unmount } = render( <Navbar />, { wrapper } )

expect( queryByTestId( 'back-button' ) ).toBeTruthy()

unmount()
} )

it( 'should render a add button', () => {
const { queryByTestId, unmount } = render( <Navbar />, { wrapper } )

expect( queryByTestId( 'add-button' ) ).toBeTruthy()

unmount()
} )

it( 'should render a Navbar with the Bookmarks Collection text', () => {
const { getByText, unmount } = render( <Navbar />, { wrapper } )

expect( getByText( 'Bookmarks Collection' ) ).toBeTruthy()

unmount()
} )
} )
38 changes: 38 additions & 0 deletions src/screens/Bookmarks/Navbar.tsx
@@ -0,0 +1,38 @@
import React from 'react'
import { StyleSheet } from 'react-native'
import IonIcon from 'react-native-vector-icons/Ionicons'
import AntIcon from 'react-native-vector-icons/AntDesign'

import Navbar from '../../components/Navbar'
import BackButton from '../../components/BackButton'
import Typography from '../../components/Typography'
import { px } from '../../themes/utils'
import Colors from '../../themes/colors'

const styles = StyleSheet.create( {
backButton: {
marginLeft: -5,
},
headerIcon: {
...px( 20 ),
fontSize: 22,
color: Colors.PrimaryText,
},
heading: {
fontSize: 16,
},
} )

/**
* Navbar component for main header.
*/
const BookmarksNavbar = () => (
<Navbar
backgroundColor="transparent"
right={<AntIcon testID="add-button" style={styles.headerIcon} name="plus" />}
main={<Typography variant="header" style={styles.heading}>Bookmarks Collection</Typography>}
left={<BackButton testID="back-button" style={styles.backButton} variant="text" label={<IonIcon style={styles.headerIcon} name="arrow-back" />} />}
/>
)

export default BookmarksNavbar

0 comments on commit 68e2aff

Please sign in to comment.