react native search and scroll to hit - reactjs

I need some help implementing a search and scroll to hit in react native. Did a lot of searches and ended up in some dead ends (found some refs examples I couldn't get to work).
Tried building this snippet as a kick-off:
https://snack.expo.io/#norfeldt/searching-and-scroll-to
import React, { Component } from 'react';
import { Text, View, ScrollView, TextInput, StyleSheet } from 'react-native';
export default class App extends Component {
state = {
text: '41'
}
render() {
return (
<View style={styles.container}>
<TextInput
style={{height: 60, borderColor: 'gray', borderWidth: 1, borderRadius: 10, margin: 5, padding:30, color: 'black', }}
onChangeText={(text) => this.setState({text})}
value={this.state.text}
/>
<ScrollView >
{[...Array(100)].map((_, i) => {return <Text style={styles.paragraph} key={i}>{i}</Text>})}
</ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 10,
backgroundColor: '#ecf0f1',
},
paragraph: {
margin: 10,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
color: '#34495e',
},
});
Any help getting started would be appreciated.

My guess would be:
You could start by binding a ref of your <ScrollView/>.
// ScrollView Ref.
<ScrollView ref={(ref) => this['ScrollView'] = ref}>
...
</ScrollView>
And each of your <Text/> components (by index).
// Text Refs.
<Text ref={(ref) => this[i] = ref} style={styles.paragraph} key={i}>{i}</Text>
You could then set a submit() function.
Said function could find the ref equal to this.state.text using a try catch statement for graceful failure in edge cases.
If found; target x and y offset could be retrieved using measure()
scrollTo() could then be called to scroll to the target component.
// Scroll To Query.
submit = () => {
try {
const { text } = this.state // Text.
const target = this[text] // Target.
// Locate Target.
target.measure((framex, framey, width, height, x, y) => {
// Scroll To Target.
this.ScrollView.scrollTo({x, y, animated: true})
})
} catch (error) {
return console.log(error)
}
}

First of all, I highly recommend you to use FlatList instead of ScrollView. There are a few reasons for this:
FlatList has much more optimized performance in comparison with ScrollView (in scroll view all items are rendered at once, regardless of the fact if they are visible on screen or not)
Moreover, the handling scrolling and rendered items is much simpler in FlatList, you do not need to know anything about x, y axis and pixels, you just work with indexes.
in order to have a comprehensive comparison between these two methods you may look at:
http://matthewsessions.com/2017/05/15/optimizing-list-render-performance.html
Now back to your question, as I said I suggest you to use FlatList, then everything will be as simple as a piece of a cake.
You can find modified example of your expo in:
https://snack.expo.io/HkMZS1SGz
The changes that you need to make in your code, include:
Instead of ScrollView use, FlatList, so change this:
<FlatList
ref={ref => {this.flatListRef = ref;}}
data={new Array(100).fill(0).map((item, index) => index)}
renderItem={({ item }) => (
<Text style={styles.paragraph}>
{item}
</Text>
)}
/>
If you are not already familiar with FlatList, you need to know, the data is added in data prop as an array (I added an array of 100 numbers), and the way it is rendered is given to FlatList as renderItemprop (I added the text with the same styling as you did).
Moreover, note that you do not need to pass ref to <Text>, because FlatList already knows about items that it contains. You just need to add a ref to the FlatList itself:
ref={ref => {this.flatListRef = ref;}}
Now when ever you want to make and scrolling, you can simple call scrollToIndex method of the FlatList, for example write a method called scrollHandler:
// Scroll to Query
scrollHandler = (itemIndex)=>{
this.flatListRef.scrollToIndex({animated: true, index: itemIndex});
}
just pay attention that,flatListRef is the name of the ref assigned to the FlatList.
now, when you want to perform scroll action, you can simply call this method. Forexample, modify your text input to:
<TextInput
style={{height: 60, borderColor: 'gray', borderWidth: 1,
borderRadius: 10, margin: 5, padding:30, color: 'black', }}
onChangeText={(text) => this.setState({text})}
value={this.state.text}
onSubmitEditing={()=>this.scrollHandler(this.state.text)}
/>

Steps
Remember every item's position with onLayout.
scrollTo() position when text input, and only if item found.
Code
const styles = StyleSheet.create({
textinput: {
borderBottomColor: 'purple',
textAlign: 'center',
borderBottomWidth: 2,
height: 40,
marginTop: 20,
},
text: {
textAlign: 'center',
fontSize: 16,
margin: 10,
}
});
export class App extends Component {
data = [];
datapos = {};
scrollref = null;
constructor(props) {
super(props);
/// make 100s example data
for (var i =0; i<100; ++i)
this.data.push(i);
this.state = {
inputvalue: '0'
}
}
render() {
return (
<View style={{flex: 1}}>
<TextInput style={styles.textinput}
value={this.state.inputvalue}
onChangeText={(text) => {
this.setState({inputvalue: text});
let y = this.datapos[+text];
y !== undefined && this.scrollref.scrollTo({ y, animated: true });
}}
/>
<ScrollView
ref={(ref) => this.scrollref = ref}
>
{
this.data.map( (data) => (
<Text style={styles.text}
key={data}
onLayout={(layout) => this.datapos[data] = layout.nativeEvent.layout.y}
>{data}</Text>
))
}
</ScrollView>
</View>
)
}
}
Result:

Related

How to optimize FlatList in React Native

Can you please tell me how can I optimize this flatlist in react native. I mean how can I do that app will render not the whole list of data but just small part of it for example 10 items, and then when the user will scroll it down it will load more of data from list?
that's the code
import React, {useState, useEffect} from 'react';
import {
SafeAreaView,
StatusBar,
StyleSheet,
Text,
View,
FlatList,
TextInput,
} from 'react-native';
import {newdata} from '../Data/newdata';
const Sample = () => {
const DATA = newdata;
const [searchText, onChangeSearch] = useState('');
const [filteredData, setFilteredData] = useState([]);
useEffect(() => {
const filtered = DATA.filter(item =>
item.title.toLowerCase().includes(searchText.toLowerCase()),
);
if (searchText === '') {
return setFilteredData(DATA);
}
setFilteredData(filtered);
}, [searchText]);
const Item = ({title}) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const renderItem = ({item}) => <Item title={item.title} />;
return (
<SafeAreaView style={styles.container}>
<TextInput
style={{
height: 50,
borderColor: '#919191',
borderWidth: 1,
margin: 10,
paddingLeft: 15,
borderRadius: 10,
}}
onChangeText={newText => onChangeSearch(newText)}
placeholder="Axtaris..."
/>
<FlatList
data={filteredData}
renderItem={renderItem}
keyExtractor={(item, index) => item.key}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
marginBottom: 75,
},
item: {
backgroundColor: '#ededed',
padding: 20,
marginVertical: 2,
marginHorizontal: 10,
borderRadius: 20,
},
title: {
fontSize: 20,
},
});
export default Sample;
P.S. newdata has about 42000 of items, and app running very slow. That is the screenshot of app
You can easily achieve that by using the initialNumToRender prop in FlatList component
How many items to render in the initial batch. This should be enough to fill the screen but not much more. Note these items will never be unmounted as part of the windowed rendering in order to improve perceived performance of scroll-to-top actions.
<FlatList
data={filteredData}
renderItem={renderItem}
keyExtractor={item => item.key}
initialNumToRender={10}
/>
Ali,
You have some features inside Flatlist in order to optimize it for example:
maxtorenderperbatch: This controls the amount of items rendered per batch, which is the next chunk of items rendered on every scroll.
Read more here
initialNumToRender: Define precise number of items that would cover the screen for every device. [Read more here][1]
Also, you can use Infinite Scroll, is very useful instead of render
the whole list you can render only amount of items, and when the user
is scrolling to the end the app load more items.
onEndReached: Called once when the scroll position gets within onEndReachedThreshold of the rendered content.
onEndReachedThreshold: How far from the end (in units of visible length of the list) the bottom edge of the list must be from the end
of the content to trigger the onEndReached callback.
Here is an example how to use it.
<FlatList
data={filteredData}
renderItem={renderItem}
keyExtractor={(item, index) => item.key}
onEndReached={ loadMoreItems }
onEndReachedThreshold={ 0.5 }
maxToRenderPerBatch={3}
initialNumToRender={5}
/>
loadMoreItems:
const loadMoreItems = ( ) => {
// Here you logic to render more items, when user scroll to the end
}

Conditional rendering cannot be seen due to a return before them

Please help give me a better title, I couldn't word what I am asking.
To understand my question I have to provide some context of my project. Originally I wanted to conditionally render two pages through two buttons. Button A rendering screen A and button B rendering screen B. After figuring out how to pass the state from a parent component to a child and its child etc, I changed my button to a sliding animation for better design.
This causes issues because now when a new screen is rendered, the animation does not show because it is simply re-rendered with the original starting place in the animation(I rendered the slider through each screen). I thought about providing two different sliders, each starting in the opposing opposition but that would still lose the entire slide effect.
I have now resulted to rendering the Slider so it is there all the time and is not re-rendered. However I have realized now that if I return it before my conditionals, that code is never reached. I have provided a working demo that shows my problem perfectly as well as the code below(I only provided App.js, the rest is on the demo if needed). I want to render Slider in App.js.
The working demo is here, you can see the slider does not slide, it just changes screens. I need it to slide. Also the sliding animation only works on iphone so I would use that emulator rather than the web.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
whichComponentToShow: "Screen1"
};
}
goToMap = () => {
this.setState({ whichComponentToShow: "Screen2" });
};
goToList = () => {
this.setState({ whichComponentToShow: "Screen1" });
};
render() {
const { whichComponentToShow } = this.state;
/* This is how I thought I could render this, but obv it makes the rest of the code unreachable.
How can I render this and then have the conditional page below? Each time the new page renders,
it stops the animation from working due to rendering the new page.
return(
<Slider/>
)*/
if(this.state.whichComponentToShow === 'Screen1'){
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
<ListHome
renderMap = {this.goToMap.bind(this)}
renderList = {this.goToList.bind(this)}
/>
</View>
);
}
else if(this.state.whichComponentToShow === 'Screen2'){
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
<MapHome
renderMap = {this.goToMap.bind(this)}
renderList = {this.goToList.bind(this)}
/>
</View>
);
}
Slider.js (wont show up on the snack apparently
const Slider = (props) => {
const [active, setActive] = useState(false)
let transformX = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (active) {
Animated.timing(transformX, {
toValue: 1,
duration: 300,
useNativeDriver: true
}).start()
} else {
Animated.timing(transformX, {
toValue: 0,
duration: 300,
useNativeDriver: true
}).start()
}
}, [active]);
const rotationX = transformX.interpolate({
inputRange: [0, 1],
outputRange: [2, Dimensions.get('screen').width / 4]
})
return (
<SafeAreaView style={{
flex: 1,
alignItems: 'center'
}}>
<View style={{
flexDirection: 'row',
position: 'relative',
height: 45,
width: 240,
borderRadius: 10,
backgroundColor: 'white',
marginHorizontal: 5
}}>
<Animated.View
style={{
position: 'absolute',
height: 45 - 2*2,
top: 2,
bottom: 2,
borderRadius: 10,
width: Dimensions
.get('screen').width / 3 - 3.5 ,
transform: [
{
translateX: rotationX
}
],
backgroundColor: '#d1cfcf',
}}
>
</Animated.View>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(false); props.renderList() }}>
<Text>
List
</Text>
</TouchableOpacity>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(true); props.renderMap() }}>
<Text>
Map
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
export default Slider
I tried your expo snack and saw no code related to animation, so I'm assuming the code in the snack isn't your current code, and that you really do already have a working, animated, <Slider ... />.
In your situation, what you could do to keep your Slider rendered and not unmounted, is to use variables in the render() method.
Basically, you can assign the <Slider .../> JSX to a variable, and you can use that variable in another JSX part later.
Assigning a key to the specific JSX also helps guide React that this is the same component between render calls, so it also prevents unintentional rerenders of that component.
Here's an edit with comments from what you wrote in your post. I hope this makes sense.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
whichComponentToShow: "Screen1"
};
}
goToMap = () => {
this.setState({ whichComponentToShow: "Screen2" });
};
goToList = () => {
this.setState({ whichComponentToShow: "Screen1" });
};
render() {
const { whichComponentToShow } = this.state;
/*
Keep the slider JSX in a variable to be used.
Setting a specific key also helps prevent it from being accidentally re-rendered in some conditions.
)*/
const sliderRender = <Slider key='slider' />;
if (this.state.whichComponentToShow === 'Screen1') {
return (
<View style={{ backgroundColor: '#d1cfcf', flex: 1 }}>
<ListHome
renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
/>
{/* Put the rendered slider into the render tree */}
{sliderRender}
</View>
);
}
else if (this.state.whichComponentToShow === 'Screen2') {
return (
<View style={{ backgroundColor: '#d1cfcf', flex: 1 }}>
<MapHome
renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
/>
{/* Put the rendered slider into the render tree */}
{sliderRender}
</View>
);
}
}
}
Edit : Expo Snack demonstrating it working

Select items and transfer selected items to another page

Hello I am creating an app, and am having difficulties trying to create a way where the user selects multiple images, which will then be passed on to another screen. Could I please get some help on this?
Much will be appreciated.
So, the way my app works is that, the user selects multiple items, then there should be an add button or a save button, that will get the selected items and display them to another screen. The items have a value which are Images, not text. This is purely the reason why I asked the question here because most of the React-Native tutorials include values based on text, rather than Images.
The problem I am having, is trying to figure out a way for the user to select multiple items, and clicking a save button, which will in return transfer all of the "selected items" to another screen to be display there. Much like a viewer.
import React, { Component } from 'react';
import { Text, View, StyleSheet, AppRegistry, FlatList, Image, TouchableOpacity } from 'react-native';
import flatListData from '../database';
class FlatListItem extends Component {
static navigationOptions = ({ navigation }) => ({
title: 'FirstScreen!'
})
render() {
return (
<View style={{
flex: 1,
flexDirection:'column',
}}>
<View style={{
flex: 1,
flexDirection:'row',
}}>
<View style={{
flex: 1,
flexDirection:'column',
height: 100
}}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('SecondScreen')} >
<Image source={{uri: this.props.item.imageUrl}}
style={{width: 100, height: 100, margin: 5}}></Image>
</TouchableOpacity>
</View>
</View>
<View style={{
height: 1,
backgroundColor:'white'
}}>
</View>
</View>
);
}
}
class FirstScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: 'First Screen'
})
render() {
return (
<View style={{flex: 1, marginTop: 22}}>
<FlatList
data={flatListData}
renderItem={({item, index})=>{
//console.log(`Item = ${JSON.stringify(item)}, index = ${index}`);
return (
<FlatListItem item={item} index={index}>
</FlatListItem>);
}}
>
</FlatList>
</View>
);
}
}
export default example;
const styles = StyleSheet.create({
flatListItem: {
color: 'white',
padding: 10,
fontSize: 16,
}
});
Since you did not provide any sample code, so I will try to suggest a way to handle via pseudocode
You can abstract out the list of images into a centralized helper class, then you render from this helper class for user to select.
Now when user have selected one of the image, you just need to capture the ID or any unique identifier, and pass it to second screen.
On this second screen, just using this ID/unique identifier that you've received and search it from the aforementioned centralized helper class and render it.
Looks like you have two things to figure out;
one is keeping track of what items a user has selected on your image selection screen
sending the data back between screens
Looks like you are most likely using react-navigation based on your example so the simplest solution would be to take advantage of React's state and use react-navigation's parameter passing between screens.
With react-navigation you can use the second argument in navigation.navigate to pass parameters/callbacks to the other screen. So you can navigate to a screen and pass a callback to it as such.
...
this.props.navigation.navigate(
'ItemSelectionScreen',
{ onSubmit: (items) => { /* Do something with items */ } }
)
...
And here is a basic example of a selection screen with some comments to explain how it works.
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'grey',
alignItems: 'center',
justifyContent: 'center'
}
});
class ItemSelectionScreen extends React.Component {
constructor(props) {
super(props);
this.onSubmit = () => props.navigation.getParam('onSubmit'); // Keep the passed callback once we have navigated to this screen
this.state = {
selectedItems: [] // Our initial selectedItems (empty)
};
}
handleToggle = (item, isSelected) => {
const { selectedItems } = this.state;
this.setState({ selectedItems: isSelected ? selectedItems.filter(ent => ent !== item) : [...selectedItems, item] }); // Toggle if an item is selected
};
handleSubmitAndExit = () => {
const { onSubmit } = this;
const { selectedItems } = this.state;
onSubmit(selectedItems); // Pass your selectedItems back to the other screen
this.props.navigation.goBack(); // And exit the screen
};
handleExit = () => {
this.props.navigation.goBack(); // Exit the screen without calling onSubmit
};
renderItem = (item, index) => {
const { selectedItems } = this.state;
const isSelected = selectedItems.some(ent => ent === item); // Determine if an item is selected
return (
<TouchableOpacity key={index} onPress={() => this.handleToggle(item, isSelected)}>
<Text>{`${isSelected ? 'X' : 'O'} ${item}`}</Text>
</TouchableOpacity>
);
};
render() {
return (
<View style={styles.container}>
{['item1', 'item2', 'item3'].map(this.renderItem)}
<TouchableOpacity onPress={this.handleSubmitAndExit}>
<Text>Submit and Exit</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.handleExit}>
<Text>Exit</Text>
</TouchableOpacity>
</View>
);
}
}
export default ItemSelectionScreen;
Good luck and hope this was helpful.

How can I change the color of an icon without re-render (TextField loosing focus)

I am using the package called react-native-phone-input, and when the phone number is changed it runs the function this.updateInfo() which updates the state. In my code, the icon color is dependent on the state and changes based on whether the phone number is valid. This works, however, when the state is changed, the screen re-renders and the keyboard is dismissed since the text field loses focus. Is there another way I can change the icons color? Or is there a way that I can keep the focus on the text field?
This is what I am referring to:
import React, { Component } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, TextInput, Keyboard} from 'react-native';
import { Icon } from 'react-native-elements';
import KeyboardAccessory from 'react-native-sticky-keyboard-accessory';
import PhoneInput from 'react-native-phone-input';
export default class PhoneLogin extends Component {
constructor() {
super();
this.state = {
valid: "",
type: "",
value: "",
iconColor: "#D3D3D3",
};
this.updateInfo = this.updateInfo.bind(this);
}
updateInfo() {
this.setState({
valid: this.phone.isValidNumber(),
type: this.phone.getNumberType(),
value: this.phone.getValue().replace(/[- )(]/g,''),
});
}
render() {
return (
<View style={styles.container}>
<PhoneInput
ref={ref => {
this.phone = ref;
}}
style={{height: 50, borderColor:'#44c0b9', borderBottomWidth:2}}
onChangePhoneNumber={ (phone) => {this.updateInfo()} }
/>
<KeyboardAccessory backgroundColor="#fff">
<View style={{ alignItems: 'flex-end', padding:10 }}>
<Icon
raised
reverse
color={(this.state.valid) ? "#44c0b9" : "#D3D3D3"}
name='arrow-right'
type='font-awesome'
onPress={ Keyboard.dismiss()}
/>
</View>
</KeyboardAccessory>
</View>
);
}
}
let styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
padding: 20,
paddingTop: 60
},
info: {
// width: 200,
borderRadius: 5,
backgroundColor: "#f0f0f0",
padding: 10,
marginTop: 20
},
button: {
marginTop: 20,
padding: 10
}
});
In your onPress of your Icon you need to send an arrow function.
The Icon should be like this
<Icon
raised
reverse
color={(this.state.valid) ? "#44c0b9" : "#D3D3D3"
name='arrow-right'
type='font-awesome'
onPress={() => Keyboard.dismiss()}
/>
The problem was the Keyboard.dismiss() was immediately running every time the component re-rendered thus dismissing your keyboard.
Hope this helps!

How to add stateless component to a touchable component

I am trying to add a stateless component to my button.
const button = ({onButtonPress, buttonText}) => {
return (
<TouchableHighlight
onPress={() => onButtonPress()}>
<ButtonContent text={buttonText}/>
</TouchableHighlight>
)
};
and get this error:
Warning: Stateless function components cannot be given refs (See ref "childRef"
in StatelessComponent created by TouchableHighlight).
Attempts to access this ref will fail.
I have read up on the issue but I am still new to javascript and RN and have not found a solution. Any help would be appreciated.
full code:
GlossaryButtonContent:
import React from 'react';
import {
View,
Text,
Image,
StyleSheet
} from 'react-native';
import Colours from '../constants/Colours';
import {
arrowForwardDark,
starDarkFill
} from '../assets/icons';
type Props = {
text: string,
showFavButton?: boolean
}
export default ({text, showFavButton} : Props) => {
return (
<View style={styles.container}>
{showFavButton &&
<Image
style={styles.star}
source={starDarkFill}/>}
<Text style={[styles.text, showFavButton && styles.favButton]}>
{showFavButton ? 'Favourites' : text}
</Text>
<Image
style={styles.image}
source={arrowForwardDark}
opacity={showFavButton ? .5 : 1}/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
alignItems: 'center'
},
favButton: {
marginLeft: 10,
color: Colours.darkTextHalf
},
text: {
flex: 1,
paddingTop: 5,
marginLeft: 20,
fontFamily: 'Bariol-Bold',
fontSize: 24,
color: Colours.darkText
},
image: {
marginRight: 20
},
star: {
marginLeft: 10
}
});
GlossaryButton:
import React from 'react';
import {
TouchableHighlight,
StyleSheet
} from 'react-native';
import Colours from '../constants/Colours';
import ShadowedBox from './ShadowedBox';
import GlossaryButtonContent from './GlossaryButtonContent';
type Props = {
buttonText: string,
onButtonPress: Function,
rowID: number,
sectionID?: string,
showFavButton?: boolean
}
export default ({buttonText, onButtonPress, rowID, sectionID, showFavButton} : Props) => {
return (
<ShadowedBox
style={styles.container}
backColor={showFavButton && Colours.yellow}>
<TouchableHighlight
style={styles.button}
underlayColor={Colours.green}
onPress={() => onButtonPress(rowID, sectionID)}>
<GlossaryButtonContent
text={buttonText}
showFavButton={showFavButton}/>
</TouchableHighlight>
</ShadowedBox>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
height: 60,
marginBottom: 10,
borderRadius: 5
},
button: {
flex: 1,
borderRadius: 5
}
});
Basically, Stateless components cannot have refs.
So, having a stateless component in the middle of the render tree will create a void in the ref chain, meaning you cannot access lower-down components.
So, the problem comes from trying to do this:
let Stateless = (props) => (
<div />
);
let Wrapper = React.createClass({
render() {
return <Stateless ref="stateeee" />
}
});
TouchableHighlight needs to give a ref to its child. And this triggers that warning.
Answer:
You can't actually make a stateless component a child of TouchableHighlight
Solution:
Use createClass or class to create the child of TouchableHighlight, that is GlossaryButtonContent.
See this github issue for more info and this one

Resources