├── .watchmanconfig ├── app ├── components │ ├── FeedNavbar │ │ ├── styles.js │ │ ├── index.js │ │ └── FeedNavbar.js │ ├── SinglePost │ │ ├── index.js │ │ ├── styles.js │ │ └── SinglePost.js │ ├── TextField │ │ ├── index.js │ │ ├── styles.js │ │ └── TextField.js │ └── SmallButton │ │ ├── index.js │ │ ├── SmallButton.js │ │ └── styles.js ├── layouts │ ├── feed │ │ ├── index.js │ │ ├── styles.js │ │ └── Feed.js │ ├── login │ │ ├── index.js │ │ ├── styles.js │ │ └── Login.js │ ├── newPost │ │ ├── index.js │ │ ├── styles.js │ │ └── NewPost.js │ ├── signup │ │ ├── index.js │ │ ├── styles.js │ │ └── Signup.js │ └── welcome │ │ ├── index.js │ │ ├── styles.js │ │ └── Welcome.js ├── assets │ └── fonts │ │ └── Pacifico.ttf ├── config │ ├── cosmic.js │ └── routes.js └── redux │ ├── reducers │ ├── index.js │ ├── posts.js │ └── users.js │ └── store.js ├── .gitignore ├── app.json ├── .package.json.swp ├── .babelrc ├── App.test.js ├── README.md ├── App.js ├── package.json ├── .flowconfig └── article.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /app/components/FeedNavbar/styles.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock/ 3 | .expo/ 4 | npm-debug.* 5 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "sdkVersion": "17.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/layouts/feed/index.js: -------------------------------------------------------------------------------- 1 | import Feed from './Feed'; 2 | 3 | export default Feed; 4 | -------------------------------------------------------------------------------- /app/layouts/login/index.js: -------------------------------------------------------------------------------- 1 | import Login from './Login'; 2 | 3 | export default Login; 4 | -------------------------------------------------------------------------------- /app/layouts/newPost/index.js: -------------------------------------------------------------------------------- 1 | import NewPost from './NewPost'; 2 | 3 | export default NewPost; 4 | -------------------------------------------------------------------------------- /app/layouts/signup/index.js: -------------------------------------------------------------------------------- 1 | import Signup from './Signup'; 2 | 3 | export default Signup; 4 | -------------------------------------------------------------------------------- /app/layouts/welcome/index.js: -------------------------------------------------------------------------------- 1 | import Welcome from './Welcome'; 2 | 3 | export default Welcome; 4 | -------------------------------------------------------------------------------- /.package.json.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-native-twitter-clone/HEAD/.package.json.swp -------------------------------------------------------------------------------- /app/components/FeedNavbar/index.js: -------------------------------------------------------------------------------- 1 | import FeedNavbar from './FeedNavbar'; 2 | 3 | export default FeedNavbar; 4 | -------------------------------------------------------------------------------- /app/components/SinglePost/index.js: -------------------------------------------------------------------------------- 1 | import SinglePost from './SinglePost'; 2 | 3 | export default SinglePost; 4 | -------------------------------------------------------------------------------- /app/components/TextField/index.js: -------------------------------------------------------------------------------- 1 | import TextField from './TextField'; 2 | 3 | export default TextField; 4 | -------------------------------------------------------------------------------- /app/components/SmallButton/index.js: -------------------------------------------------------------------------------- 1 | import SmallButton from './SmallButton'; 2 | 3 | export default SmallButton; 4 | -------------------------------------------------------------------------------- /app/assets/fonts/Pacifico.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-native-twitter-clone/HEAD/app/assets/fonts/Pacifico.ttf -------------------------------------------------------------------------------- /app/components/TextField/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | big: { 3 | height: 200, 4 | }, 5 | small: { 6 | height: 45, 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/config/cosmic.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bucket: { 3 | slug: process.env.COSMIC_BUCKET || 'react-native-twitter-clone', 4 | read_key: process.env.COSMIC_READ_KEY, 5 | write_key: process.env.COSMIC_WRITE_KEY 6 | } 7 | } -------------------------------------------------------------------------------- /app/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | const rootReducer = combineReducers({ 4 | user: require('./users').default, 5 | posts: require('./posts').default, 6 | }) 7 | 8 | export default rootReducer; 9 | -------------------------------------------------------------------------------- /App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | 4 | import renderer from 'react-test-renderer'; 5 | 6 | it('renders without crashing', () => { 7 | const rendered = renderer.create().toJSON(); 8 | expect(rendered).toBeTruthy(); 9 | }); 10 | -------------------------------------------------------------------------------- /app/layouts/feed/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | container: { 3 | marginTop: 64, 4 | }, 5 | button: { 6 | position: 'absolute', 7 | bottom: 20, 8 | right: 20, 9 | }, 10 | end: { 11 | margin: 10, 12 | alignSelf: 'center', 13 | fontSize: 12, 14 | color: 'grey', 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /app/components/SinglePost/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | nameContainer: { 3 | flex: 1, 4 | flexDirection: 'row', 5 | }, 6 | name: { 7 | fontWeight: '600', 8 | fontSize: 14, 9 | }, 10 | username: { 11 | fontWeight: '200', 12 | fontSize: 12, 13 | }, 14 | content: { 15 | fontSize: 16, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /app/layouts/newPost/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | container: { 3 | marginTop: 75, 4 | }, 5 | input: { 6 | margin: 15, 7 | flex: 1, 8 | }, 9 | button: { 10 | alignSelf: 'flex-end', 11 | margin: 20, 12 | }, 13 | formMsg: { 14 | fontSize: 10, 15 | color: 'red', 16 | alignSelf: 'center', 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /app/components/SmallButton/SmallButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-native-elements'; 3 | import { styles } from './styles'; 4 | 5 | export default (props) => ( 6 | 22 | 23 | 24 | Your feed 25 | 26 | 27 | 33 | 34 | 35 | ) 36 | -------------------------------------------------------------------------------- /app/config/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Scene, Router, Actions, ActionConst } from 'react-native-router-flux'; 3 | import Welcome from '../layouts/welcome'; 4 | import Login from '../layouts/login'; 5 | import Signup from '../layouts/signup'; 6 | import NewPost from '../layouts/newPost'; 7 | import Feed from '../layouts/feed'; 8 | 9 | const scenes = Actions.create( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export default () => ( 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AppRegistry, View } from 'react-native'; 3 | import { Provider, connect } from 'react-redux'; 4 | import { Font, AppLoading } from 'expo'; 5 | import store from './app/redux/store'; 6 | import Router from './app/config/routes'; 7 | 8 | export default class App extends Component { 9 | constructor(){ 10 | super(); 11 | this.state = { 12 | isReady: false, 13 | } 14 | } 15 | 16 | async componentWillMount() { 17 | await Font.loadAsync({ 18 | 'Roboto': require('native-base/Fonts/Roboto.ttf'), 19 | 'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'), 20 | 'Pacifico': require('./app/assets/fonts/Pacifico.ttf'), 21 | 'Ionicons': require('native-base/Fonts/Ionicons.ttf'), 22 | }); 23 | 24 | this.setState({isReady: true}); 25 | } 26 | 27 | 28 | render() { 29 | if (!this.state.isReady) { 30 | return ; 31 | } 32 | return ( 33 | 34 | 35 | 36 | ); 37 | } 38 | } 39 | 40 | AppRegistry.registerComponent('main', () => App); 41 | -------------------------------------------------------------------------------- /app/layouts/welcome/Welcome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | Content, 5 | Icon, 6 | Text, 7 | Button, 8 | } from 'native-base'; 9 | import { View } from 'react-native'; 10 | import { Actions } from 'react-native-router-flux'; 11 | 12 | import styles from './styles'; 13 | 14 | 15 | export default () => ( 16 | 17 | 18 | 19 | 24 | Welcome 25 | 26 | 27 | 34 | OR 35 | 42 | 43 | 44 | 45 | ) 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-clone", 3 | "version": "1.0.0", 4 | "private": true, 5 | "devDependencies": { 6 | "concurrently": "^3.4.0", 7 | "jest-expo": "~1.0.1", 8 | "react-native-scripts": "0.0.30", 9 | "react-test-renderer": "16.0.0-alpha.6" 10 | }, 11 | "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js", 12 | "scripts": { 13 | "start": "concurrently \"react-native-scripts start\" \"node appserver.js\"", 14 | "eject": "react-native-scripts eject", 15 | "android": "react-native-scripts android", 16 | "ios": "react-native-scripts ios", 17 | "test": "node node_modules/jest/bin/jest.js --watch" 18 | }, 19 | "jest": { 20 | "preset": "jest-expo" 21 | }, 22 | "dependencies": { 23 | "@expo/vector-icons": "^5.0.0", 24 | "axios": "^0.16.1", 25 | "body-parser": "^1.17.2", 26 | "expo": "^17.0.0", 27 | "express": "^4.15.3", 28 | "form-data": "^2.2.0", 29 | "mv": "^2.1.1", 30 | "native-base": "^2.1.4", 31 | "react": "16.0.0-alpha.6", 32 | "react-native": "^0.44.0", 33 | "react-native-router-flux": "^3.39.2", 34 | "react-redux": "^5.0.5", 35 | "redux": "^3.6.0", 36 | "redux-devtools-extension": "^2.13.2", 37 | "redux-logger": "^3.0.6", 38 | "redux-thunk": "^2.2.0", 39 | "socket.io": "^2.0.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/layouts/newPost/NewPost.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | Container, 5 | Content, 6 | Text, 7 | Button, 8 | } from 'native-base'; 9 | import { View } from 'react-native'; 10 | import TextField from '../../components/TextField'; 11 | import styles from './styles'; 12 | 13 | import { createPost } from '../../redux/reducers/posts'; 14 | 15 | const mapStateToProps = state => ({ 16 | user: state.user, 17 | }) 18 | 19 | const mapDispatchToProps = { createPost }; 20 | 21 | class NewPost extends Component { 22 | constructor(){ 23 | super(); 24 | this.state = { 25 | content: '', 26 | error: '', 27 | } 28 | } 29 | onSubmit() { 30 | if (this.state.content){ 31 | this.props.createPost({ 32 | user: this.props.user, 33 | content: this.state.content, 34 | }) 35 | } else { 36 | this.setState({error: 'You have to write something!'}); 37 | } 38 | } 39 | 40 | render(){ 41 | return ( 42 | 43 | 44 | {this.state.error} 45 | 46 | this.setState({content: text})} 51 | /> 52 | 59 | 60 | 61 | 62 | ); 63 | } 64 | } 65 | 66 | export default connect(mapStateToProps, mapDispatchToProps)(NewPost); 67 | -------------------------------------------------------------------------------- /app/layouts/feed/Feed.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Actions } from 'react-native-router-flux'; 4 | import { 5 | Container, 6 | Content, 7 | List, 8 | Button, 9 | Icon, 10 | Text, 11 | } from 'native-base'; 12 | 13 | import SinglePost from '../../components/SinglePost'; 14 | import FeedNavbar from '../../components/FeedNavbar'; 15 | import { loadPosts } from '../../redux/reducers/posts'; 16 | import { logoutUser } from '../../redux/reducers/users'; 17 | import styles from './styles'; 18 | 19 | const mapStateToProps = ({ posts }) => ({ posts }); 20 | 21 | const mapDispatchToProps = { loadPosts, logoutUser }; 22 | 23 | const renderPost = (post, index) => ( 24 | 31 | ) 32 | 33 | class Feed extends Component { 34 | componentDidMount(){ 35 | this.props.loadPosts(); 36 | } 37 | 38 | render(){ 39 | const endMsg = this.props.posts.length === 0 ? "There aren't any posts yet!" : "That's all the posts for now!" 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | { 47 | !!this.props.posts.length && this.props.posts.map(renderPost) 48 | } 49 | 50 | {endMsg} 51 | 52 | 62 | 63 | ); 64 | } 65 | } 66 | 67 | export default connect(mapStateToProps, mapDispatchToProps)(Feed); 68 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | ; Additional create-react-native-app ignores 18 | 19 | ; Ignore duplicate module providers 20 | .*/node_modules/fbemitter/lib/* 21 | 22 | ; Ignore misbehaving dev-dependencies 23 | .*/node_modules/xdl/build/* 24 | .*/node_modules/reqwest/tests/* 25 | 26 | ; Ignore missing expo-sdk dependencies (temporarily) 27 | ; https://github.com/exponent/exponent-sdk/issues/36 28 | .*/node_modules/expo/src/* 29 | 30 | ; Ignore react-native-fbads dependency of the expo sdk 31 | .*/node_modules/react-native-fbads/* 32 | 33 | [include] 34 | 35 | [libs] 36 | node_modules/react-native/Libraries/react-native/react-native-interface.js 37 | node_modules/react-native/flow 38 | flow/ 39 | 40 | [options] 41 | module.system=haste 42 | 43 | emoji=true 44 | 45 | experimental.strict_type_args=true 46 | 47 | munge_underscores=true 48 | 49 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 50 | 51 | suppress_type=$FlowIssue 52 | suppress_type=$FlowFixMe 53 | suppress_type=$FixMe 54 | 55 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-2]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 56 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-2]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 57 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 58 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 59 | 60 | unsafe.enable_getters_and_setters=true 61 | 62 | [version] 63 | ^0.42.0 64 | -------------------------------------------------------------------------------- /app/redux/reducers/posts.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Actions } from 'react-native-router-flux'; 3 | import cosmicConfig from '../../config/cosmic'; 4 | 5 | // Constants 6 | const INITIALIZE = 'INITIALIZE_POSTS'; 7 | const CREATE = 'CREATE_POST'; 8 | const CLEAR = 'CLEAR'; 9 | 10 | // Action Creators 11 | const init = posts => ({ type: INITIALIZE, posts }); 12 | const create = post => ({ type: CREATE, post }); 13 | export const clear = () => ({ type: CLEAR }); 14 | 15 | // Reducer 16 | export default (posts = [], action) => { 17 | switch (action.type) { 18 | case INITIALIZE: 19 | return action.posts; 20 | case CREATE: 21 | return [action.post, ...posts]; 22 | case CLEAR: 23 | return []; 24 | default: 25 | return posts; 26 | } 27 | } 28 | 29 | // Helper Functions 30 | const formatPosts = data => data.map(post => { 31 | const user = post.metadata.user.metadata; 32 | 33 | return { 34 | name: user.name, 35 | username: user.username, 36 | profilePicture: {uri: user.profile_picture.url}, 37 | content: post.content.replace(/<[^>]*>/g, ''), 38 | created: post.created, 39 | } 40 | }) 41 | 42 | const formatPost = (response, postData) => { 43 | const post = response.object; 44 | const user = postData.user; 45 | 46 | return { 47 | name: user.name, 48 | username: user.username, 49 | profilePicture: { uri: user.profilePicture.url }, 50 | content: post.content, 51 | created: post.created, 52 | } 53 | } 54 | 55 | const postSorter = (a, b) => { 56 | return new Date(b.created) - new Date(a.created); 57 | } 58 | 59 | // Dispatcher 60 | export const loadPosts = () => dispatch => { 61 | return axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/posts`) 62 | .then(res => res.data.objects ? formatPosts(res.data.objects) : []) 63 | .then(formattedPosts => formattedPosts.sort(postSorter)) 64 | .then(sortedPosts => dispatch(init(sortedPosts))) 65 | .catch(err => console.error(`Could not load posts`, err)); 66 | }; 67 | 68 | export const createPost = post => dispatch => { 69 | return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/add-object`, { 70 | title: post.user.username + ' post', 71 | type_slug: 'posts', 72 | content: post.content, 73 | metafields: [ 74 | { 75 | type: 'object', 76 | title: 'User', 77 | key: 'user', 78 | object_type: 'users', 79 | value: post.user.id 80 | }, 81 | ] 82 | }) 83 | .then(res => formatPost(res.data, post)) 84 | .then(formattedPost => dispatch(create(formattedPost))) 85 | .then(() => Actions.feed({type: 'popAndReplace'})) 86 | .catch(error => console.error('Post unsuccessful', error)) 87 | } 88 | -------------------------------------------------------------------------------- /app/layouts/login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | Container, 5 | Content, 6 | Icon, 7 | Text, 8 | Button, 9 | } from 'native-base'; 10 | import { View } from 'react-native'; 11 | import { Actions } from 'react-native-router-flux'; 12 | import TextField from '../../components/TextField'; 13 | import styles from './styles'; 14 | 15 | import { authenticate } from '../../redux/reducers/users'; 16 | 17 | const mapDispatchToProps = {authenticate}; 18 | 19 | const validate = form => { 20 | let errorMessage = ''; 21 | if (form.username.includes(' ') || form.password.includes(' ')){ 22 | errorMessage = 'Username and password cannot contain spaces'; 23 | } 24 | if (form.username === '' || form.password === ''){ 25 | errorMessage = 'All fields must be filled'; 26 | } 27 | return errorMessage; 28 | } 29 | 30 | class Login extends Component { 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | username: '', 35 | password: '', 36 | error: '', 37 | }; 38 | } 39 | 40 | onSubmit(){ 41 | const error = validate(this.state); 42 | if (error) { 43 | this.setState({ error }) 44 | } else { 45 | this.login(); 46 | } 47 | } 48 | 49 | login(){ 50 | this.props.authenticate(this.state) 51 | .then(res => { 52 | if (res === 'Username invalid' || res === 'Password invalid'){ 53 | this.setState({ 54 | error: res, 55 | username: '', 56 | password: '', 57 | }) 58 | } else { 59 | Actions.feed(); 60 | } 61 | }); 62 | } 63 | 64 | render(){ 65 | return ( 66 | 67 | 68 | {this.state.error} 69 | 74 | 75 | this.setState({username: text})} 80 | /> 81 | this.setState({password: text})} 87 | /> 88 | 95 | 96 | 102 | 103 | 104 | ); 105 | } 106 | } 107 | 108 | export default connect(null, mapDispatchToProps)(Login); 109 | -------------------------------------------------------------------------------- /app/redux/reducers/users.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import cosmicConfig from '../../config/cosmic'; 3 | import FormData from 'form-data'; 4 | import { Actions } from 'react-native-router-flux'; 5 | import { clear } from './posts'; 6 | 7 | // Constants 8 | const CREATE_USER = 'CREATE_USER'; 9 | const LOGIN = 'LOGIN'; 10 | const LOGOUT = 'LOGOUT'; 11 | 12 | // Action Creators 13 | const createUser = user => ({ type: CREATE_USER, user }); 14 | const login = user => ({ type: LOGIN, user }); 15 | const logout = () => ({ type: LOGOUT }); 16 | 17 | // Reducer 18 | export default (user = {}, action) => { 19 | switch (action.type) { 20 | case CREATE_USER: 21 | return action.user; 22 | case LOGIN: 23 | return action.user; 24 | case LOGOUT: 25 | return {}; 26 | default: 27 | return user; 28 | } 29 | } 30 | 31 | // Helper Function 32 | const formatUser = data => ({ 33 | name: data.object.metadata.name, 34 | username: data.object.metadata.username, 35 | profilePicture: data.object.metadata.profile_picture, 36 | id: data.object._id, 37 | slug: data.object.slug, 38 | }) 39 | 40 | // Dispatcher 41 | export const addUser = user => dispatch => { 42 | let data = new FormData(); 43 | data.append('media', { 44 | uri: user.image, 45 | type: 'image/jpeg', 46 | name: 'image' 47 | }); 48 | 49 | return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/media`, data) 50 | .then(res => res.data.media) 51 | .then(media => { 52 | return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/add-object`, { 53 | title: user.firstName + ' ' + user.lastName, 54 | type_slug: 'users', 55 | metafields: [ 56 | { 57 | key: 'name', 58 | type: 'text', 59 | value: user.firstName + ' ' + user.lastName, 60 | }, 61 | { 62 | key: 'username', 63 | type: 'text', 64 | value: user.username, 65 | }, 66 | { 67 | key: 'password', 68 | type: 'text', 69 | value: user.password, 70 | }, 71 | { 72 | key: 'profile_picture', 73 | type: 'file', 74 | value: media.name, 75 | } 76 | ] 77 | } 78 | )} 79 | ) 80 | .then(res => formatUser(res.data)) 81 | .then(formattedUser => dispatch(createUser(formattedUser))) 82 | .then(() => Actions.feed()) 83 | .catch(err => console.error(`Creating user unsuccessful`, err)) 84 | } 85 | 86 | export const authenticate = user => dispatch => { 87 | return axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/users/search?metafield_key=username&metafield_value=${user.username}`) 88 | .then(res => res.data) 89 | .then(data => { 90 | console.log('RESPONSE: ', data); 91 | if (data.objects) { 92 | const userData = data.objects[0]; 93 | return { 94 | password: userData.metadata.password, 95 | username: userData.metadata.username, 96 | name: userData.metadata.name, 97 | profilePicture: userData.metadata.profile_picture, 98 | slug: userData.slug, 99 | id: userData._id, 100 | } 101 | } else { 102 | return 'Username invalid'; 103 | } 104 | }) 105 | .then(data => { 106 | if (data === 'Username invalid'){ 107 | return data; 108 | } else if (data.password === user.password){ 109 | dispatch(login({ 110 | name: data.name, 111 | username: data.username, 112 | profilePicture: data.profilePicture, 113 | slug: data.slug, 114 | id: data.id, 115 | })) 116 | } else { 117 | return 'Password invalid'; 118 | } 119 | }) 120 | .catch(error => console.error('Login unsuccessful', error)) 121 | } 122 | 123 | export const logoutUser = () => dispatch => { 124 | dispatch(logout()); 125 | dispatch(clear()); 126 | Actions.welcome(); 127 | } 128 | -------------------------------------------------------------------------------- /app/layouts/signup/Signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { ImagePicker } from 'expo'; 4 | import { Actions } from 'react-native-router-flux'; 5 | import {View} from 'react-native'; 6 | import { 7 | Container, 8 | Content, 9 | Button, 10 | Text, 11 | Form, 12 | Thumbnail, 13 | Icon 14 | } from 'native-base'; 15 | import axios from 'axios'; 16 | 17 | import TextField from '../../components/TextField'; 18 | import styles from './styles'; 19 | import { addUser } from '../../redux/reducers/users'; 20 | import cosmicConfig from '../../config/cosmic'; 21 | 22 | const mapDispatchToProps = {addUser}; 23 | 24 | const validate = form => { 25 | let errorMessage = ''; 26 | if (form.username.includes(" ")){ 27 | errorMessage = "Username cannot contain spaces"; 28 | } 29 | if (form.password.includes(" ")){ 30 | errorMessage = "Password cannot contain spaces"; 31 | } 32 | Object.keys(form).slice(0, 5).map(field => { 33 | if (!form[field]){ 34 | errorMessage = 'All fields must be filled'; 35 | } 36 | }) 37 | return errorMessage; 38 | } 39 | 40 | class Signup extends Component { 41 | constructor() { 42 | super(); 43 | this.state = { 44 | firstName: '', 45 | lastName: '', 46 | username: '', 47 | password: '', 48 | image: null, 49 | error: '', 50 | }; 51 | } 52 | 53 | onSubmit(){ 54 | const error = validate(this.state); 55 | if (error) { 56 | this.setState({ error }) 57 | } else { 58 | this.checkUsername(this.state.username); 59 | } 60 | } 61 | 62 | checkUsername(username){ 63 | axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/users/search?metafield_key=username&metafield_value=${username}`) 64 | .then(res => res.data) 65 | .then(data => { 66 | if (data.objects) { 67 | this.setState({ error: 'Username not available'}) 68 | } else { 69 | this.props.addUser(this.state); 70 | } 71 | }) 72 | } 73 | 74 | uploadImage = async () => { 75 | let result = await ImagePicker.launchImageLibraryAsync({ 76 | allowsEditing: true, 77 | aspect: [4, 3], 78 | }); 79 | if (!result.cancelled) { 80 | this.setState({ image: result.uri }); 81 | } 82 | }; 83 | 84 | render(){ 85 | return ( 86 | 87 | 88 |
89 | this.setState({firstName: text})} 93 | /> 94 | this.setState({lastName: text})} 98 | /> 99 | this.setState({username: text})} 103 | /> 104 | this.setState({password: text})} 109 | /> 110 | 111 | Add a profile picture 112 | { 113 | !this.state.image && 114 | 124 | } 125 | { 126 | this.state.image && 127 | 132 | } 133 | 140 | {this.state.error} 141 | 148 |
149 |
150 | ); 151 | } 152 | } 153 | 154 | export default connect(null, mapDispatchToProps)(Signup); 155 | -------------------------------------------------------------------------------- /article.md: -------------------------------------------------------------------------------- 1 | # How to make a simple Twitter clone with React Native 2 | 3 | In this tutorial, we're going to create a small Twitter-like mobile app using React Native. With our app, users will be able to create accounts and log in, see a feed of all of the posts created by themselves and other users, and add their own posts to the feed. The data for all of our users and posts will be managed by Cosmic JS. 4 | 5 | For the full source code, go [here](http://github.com/thenorthstarblues/react-native-cosmic-app). 6 | 7 | ## Getting Started 8 | 9 | You’ll need to have node.js and yarn or npm installed. For this project I'm using yarn and node v8.1.2. 10 | 11 | There are a number of ways to start a new React Native project; for this one, I used [create-react-native-app](https://github.com/react-community/create-react-native-app). We can globally install CRNA and fire up a new project by running the following commands: 12 | ``` 13 | $ yarn global add create-react-native-app 14 | $ create-react-native-app twitter-clone 15 | $ cd twitter-clone/ 16 | $ yarn start 17 | ``` 18 | 19 | I'm also using the [Expo SDK](expo.io) so that I don't have to get Xcode and Android Studio set up. This is also going to provide me with features that will help with things like loading fonts and allowing users to upload photos to the app. To get started with Expo, please refer to the docs. 20 | 21 | ## Dependencies 22 | 23 | We're going to use several tools for this project; we'll talk about a few of the key ones here. 24 | 25 | * [Native Base](https://nativebase.io/) is a component library that will allow us to quickly make an attractive user interface that works cross-platform. 26 | * [React Native Router Flux](https://github.com/aksonov/react-native-router-flux) will help us navigate between the different screens of our app. 27 | * [React-Redux](https://github.com/reactjs/react-redux) will connect the different components of our app to our store where we will keep data about the state of our application. 28 | * [Axios](https://github.com/mzabriskie/axios) is a promised-based HTTP client that we will use to make our calls to the Cosmic JS API. 29 | 30 | Go ahead and copy and paste the following into your package.json and then run `yarn install` again. 31 | 32 | ``` 33 | { 34 | "name": "twitter-clone", 35 | "version": "1.0.0", 36 | "private": true, 37 | "devDependencies": { 38 | "jest-expo": "~1.0.1", 39 | "react-native-scripts": "0.0.30", 40 | "react-test-renderer": "16.0.0-alpha.6" 41 | }, 42 | "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js", 43 | "scripts": { 44 | "start": ""react-native-scripts start", 45 | "eject": "react-native-scripts eject", 46 | "android": "react-native-scripts android", 47 | "ios": "react-native-scripts ios", 48 | "test": "node node_modules/jest/bin/jest.js --watch" 49 | }, 50 | "jest": { 51 | "preset": "jest-expo" 52 | }, 53 | "dependencies": { 54 | "@expo/vector-icons": "^5.0.0", 55 | "axios": "^0.16.1", 56 | "expo": "^17.0.0", 57 | "form-data": "^2.2.0", 58 | "native-base": "^2.1.4", 59 | "react": "16.0.0-alpha.6", 60 | "react-native": "^0.44.0", 61 | "react-native-router-flux": "^3.39.2", 62 | "react-redux": "^5.0.5", 63 | "redux": "^3.6.0", 64 | "redux-devtools-extension": "^2.13.2", 65 | "redux-logger": "^3.0.6", 66 | "redux-thunk": "^2.2.0", 67 | } 68 | } 69 | ``` 70 | 71 | ## Directory Structure 72 | 73 | Compared to some other boilerplates, CRNA is pretty unopinionated about how we structure the files in our application; it just gives us an `index.ios.js` and `index.android.js` and `App.js` as a starting point. 74 | 75 | We're going to have our `App.js` point to a folder called `app` that will hold all of our components, layouts, config files, and our redux store and reducers. The following is the scaffolding that I have found works best for me. I won't go into the contents of each and every file in this post, but you can see it all in the [source code](http://github.com/thenorthstarblues/react-native-cosmic-app). 76 | 77 | Here is what our `app` folder will look like: 78 | 79 | ``` 80 | . 81 | ├── assets 82 | │   └── fonts 83 | │   └── Pacifico.ttf 84 | ├── components 85 | │   ├── FeedNavbar 86 | │   │   ├── FeedNavbar.js 87 | │   │   ├── index.js 88 | │   │   └── styles.js 89 | │   ├── SinglePost 90 | │   │   ├── SinglePost.js 91 | │   │   ├── index.js 92 | │   │   └── styles.js 93 | │   ├── SmallButton 94 | │   │   ├── SmallButton.js 95 | │   │   ├── index.js 96 | │   │   └── styles.js 97 | │   └── TextField 98 | │   ├── TextField.js 99 | │   ├── index.js 100 | │   └── styles.js 101 | ├── config 102 | │   ├── cosmic.js 103 | │   └── routes.js 104 | ├── layouts 105 | │   ├── feed 106 | │   │   ├── Feed.js 107 | │   │   ├── index.js 108 | │   │   └── styles.js 109 | │   ├── login 110 | │   │   ├── Login.js 111 | │   │   ├── index.js 112 | │   │   └── styles.js 113 | │   ├── newPost 114 | │   │   ├── NewPost.js 115 | │   │   ├── index.js 116 | │   │   └── styles.js 117 | │   ├── signup 118 | │   │   ├── Signup.js 119 | │   │   ├── index.js 120 | │   │   └── styles.js 121 | │   └── welcome 122 | │   ├── Welcome.js 123 | │   ├── index.js 124 | │   └── styles.js 125 | └── redux 126 | ├── reducers 127 | │   ├── index.js 128 | │   ├── posts.js 129 | │   └── users.js 130 | └── store.js 131 | ``` 132 | 133 | ## App 134 | 135 | A few things are going to happen in our `App.js` file. We will: 136 | 137 | * Pull in our routes which navigate to our various layouts 138 | * Connect our provider to our store which will let our layouts access our application state 139 | * Provide access to some fonts that Natve Base uses 140 | * Establish our root component with `AppRegistry` 141 | 142 | You can copy and paste the following into your `App.js` file in the project root: 143 | 144 | ``` javascript 145 | import React, { Component } from 'react'; 146 | import { AppRegistry, View } from 'react-native'; 147 | import { Provider, connect } from 'react-redux'; 148 | import { Font, AppLoading } from 'expo'; 149 | import store from './app/redux/store'; 150 | import Router from './app/config/routes'; 151 | 152 | export default class App extends Component { 153 | constructor(){ 154 | super(); 155 | this.state = { 156 | isReady: false, 157 | } 158 | } 159 | 160 | async componentWillMount() { 161 | await Font.loadAsync({ 162 | 'Roboto': require('native-base/Fonts/Roboto.ttf'), 163 | 'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'), 164 | 'Pacifico': require('./app/assets/fonts/Pacifico.ttf'), 165 | 'Ionicons': require('native-base/Fonts/Ionicons.ttf'), 166 | }); 167 | 168 | this.setState({isReady: true}); 169 | } 170 | 171 | 172 | render() { 173 | if (!this.state.isReady) { 174 | return ; 175 | } 176 | return ( 177 | 178 | 179 | 180 | ); 181 | } 182 | } 183 | 184 | AppRegistry.registerComponent('main', () => App); 185 | ``` 186 | 187 | Next, let's take a look at our `routes.js` file: 188 | 189 | ``` javascript 190 | import React from 'react'; 191 | import { Scene, Router, Actions, ActionConst } from 'react-native-router-flux'; 192 | import Welcome from '../layouts/welcome'; 193 | import Login from '../layouts/login'; 194 | import Signup from '../layouts/signup'; 195 | import NewPost from '../layouts/newPost'; 196 | import Feed from '../layouts/feed'; 197 | 198 | const scenes = Actions.create( 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | ); 207 | 208 | export default () => ( 209 | 210 | ); 211 | ``` 212 | 213 | Using React Native Router Flux, we've just created a bunch of scenes to which we can easily navigate from anywhere in our app. 214 | 215 | Our first scene is the `Welcome` layout, where users will choose between logging in and creating a new account. It looks like this: 216 | 217 | ``` javascript 218 | import React from 'react'; 219 | import { 220 | Container, 221 | Content, 222 | Icon, 223 | Text, 224 | Button, 225 | } from 'native-base'; 226 | import { View } from 'react-native'; 227 | import { Actions } from 'react-native-router-flux'; 228 | 229 | import styles from './styles'; 230 | 231 | 232 | export default () => ( 233 | 234 | 235 | 236 | 241 | Welcome 242 | 243 | 244 | 251 | OR 252 | 259 | 260 | 261 | 262 | ) 263 | ``` 264 | Here we've just created two buttons with Native Base that will navigate to the `Login` and `Signup` layouts. 265 | 266 | Let's take a look at our `Signup` layout and see what happens when users create a new account. 267 | 268 | ``` javascript 269 | import React, { Component } from 'react'; 270 | import { connect } from 'react-redux'; 271 | import { ImagePicker } from 'expo'; 272 | import { Actions } from 'react-native-router-flux'; 273 | import {View} from 'react-native'; 274 | import { 275 | Container, 276 | Content, 277 | Button, 278 | Text, 279 | Form, 280 | Thumbnail, 281 | Icon 282 | } from 'native-base'; 283 | import axios from 'axios'; 284 | 285 | import TextField from '../../components/TextField'; 286 | import styles from './styles'; 287 | import { addUser } from '../../redux/reducers/users'; 288 | import cosmicConfig from '../../config/cosmic'; 289 | 290 | const mapDispatchToProps = {addUser}; 291 | 292 | const validate = form => { 293 | let errorMessage = ''; 294 | if (form.username.includes(" ")){ 295 | errorMessage = "Username cannot contain spaces"; 296 | } 297 | if (form.password.includes(" ")){ 298 | errorMessage = "Password cannot contain spaces"; 299 | } 300 | Object.keys(form).slice(0, 5).map(field => { 301 | if (!form[field]){ 302 | errorMessage = 'All fields must be filled'; 303 | } 304 | }) 305 | return errorMessage; 306 | } 307 | 308 | class Signup extends Component { 309 | constructor() { 310 | super(); 311 | this.state = { 312 | firstName: '', 313 | lastName: '', 314 | username: '', 315 | password: '', 316 | image: null, 317 | error: '', 318 | }; 319 | } 320 | 321 | onSubmit(){ 322 | const error = validate(this.state); 323 | if (error) { 324 | this.setState({ error }) 325 | } else { 326 | this.checkUsername(this.state.username); 327 | } 328 | } 329 | 330 | checkUsername(username){ 331 | axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/users/search?metafield_key=username&metafield_value=${username}`) 332 | .then(res => res.data) 333 | .then(data => { 334 | if (data.objects) { 335 | this.setState({ error: 'Username not available'}) 336 | } else { 337 | this.props.addUser(this.state); 338 | } 339 | }) 340 | } 341 | 342 | uploadImage = async () => { 343 | let result = await ImagePicker.launchImageLibraryAsync({ 344 | allowsEditing: true, 345 | aspect: [4, 3], 346 | }); 347 | if (!result.cancelled) { 348 | this.setState({ image: result.uri }); 349 | } 350 | }; 351 | 352 | render(){ 353 | return ( 354 | 355 | 356 |
357 | this.setState({firstName: text})} 361 | /> 362 | this.setState({lastName: text})} 366 | /> 367 | this.setState({username: text})} 371 | /> 372 | this.setState({password: text})} 377 | /> 378 | 379 | Add a profile picture 380 | { 381 | !this.state.image && 382 | 392 | } 393 | { 394 | this.state.image && 395 | 400 | } 401 | 408 | {this.state.error} 409 | 416 |
417 |
418 | ); 419 | } 420 | } 421 | 422 | export default connect(null, mapDispatchToProps)(Signup); 423 | ``` 424 | 425 | There are a couple of things that happen here: 426 | * We keep the contents of our form fields on state as the users fill out the form. 427 | * When users submit, we do some simple validation to make sure that they have filled out the fields with valid input. 428 | * We then make our first call to the Cosmic JS API to make sure that the username they have selected is not already in use. 429 | * Finally, when all of the fields contain valid input, we submit the form as a new user to the Cosmic JS API with our `addUser` function. 430 | 431 | The `addUser` function is defined in our `users` reducer; it looks like this: 432 | 433 | ``` javascript 434 | export const addUser = user => dispatch => { 435 | let data = new FormData(); 436 | data.append('media', { 437 | uri: user.image, 438 | type: 'image/jpeg', 439 | name: 'image' 440 | }); 441 | 442 | return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/media`, data) 443 | .then(res => res.data.media) 444 | .then(media => { 445 | return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/add-object`, { 446 | title: user.firstName + ' ' + user.lastName, 447 | type_slug: 'users', 448 | metafields: [ 449 | { 450 | key: 'name', 451 | type: 'text', 452 | value: user.firstName + ' ' + user.lastName, 453 | }, 454 | { 455 | key: 'username', 456 | type: 'text', 457 | value: user.username, 458 | }, 459 | { 460 | key: 'password', 461 | type: 'text', 462 | value: user.password, 463 | }, 464 | { 465 | key: 'profile_picture', 466 | type: 'file', 467 | value: media.name, 468 | } 469 | ] 470 | } 471 | )} 472 | ) 473 | .then(res => formatUser(res.data)) 474 | .then(formattedUser => dispatch(createUser(formattedUser))) 475 | .then(() => Actions.feed()) 476 | .catch(err => console.error(`Creating user unsuccessful`, err)) 477 | } 478 | ``` 479 | 480 | Here we make two calls to the Cosmic JS API. The first call will post the user's profile picture to our bucket as Media, and the second will use the reference to the picture that we get back to post all of the user's information as a new user. 481 | 482 | If the user has already created an account, they can login: 483 | 484 | ``` javascript 485 | import React, { Component } from 'react'; 486 | import { connect } from 'react-redux'; 487 | import { 488 | Container, 489 | Content, 490 | Icon, 491 | Text, 492 | Button, 493 | } from 'native-base'; 494 | import { View } from 'react-native'; 495 | import { Actions } from 'react-native-router-flux'; 496 | import TextField from '../../components/TextField'; 497 | import styles from './styles'; 498 | 499 | import { authenticate } from '../../redux/reducers/users'; 500 | 501 | const mapDispatchToProps = {authenticate}; 502 | 503 | const validate = form => { 504 | let errorMessage = ''; 505 | if (form.username.includes(' ') || form.password.includes(' ')){ 506 | errorMessage = 'Username and password cannot contain spaces'; 507 | } 508 | if (form.username === '' || form.password === ''){ 509 | errorMessage = 'All fields must be filled'; 510 | } 511 | return errorMessage; 512 | } 513 | 514 | class Login extends Component { 515 | constructor(props) { 516 | super(props); 517 | this.state = { 518 | username: '', 519 | password: '', 520 | error: '', 521 | }; 522 | } 523 | 524 | onSubmit(){ 525 | const error = validate(this.state); 526 | if (error) { 527 | this.setState({ error }) 528 | } else { 529 | this.login(); 530 | } 531 | } 532 | 533 | login(){ 534 | this.props.authenticate(this.state) 535 | .then(res => { 536 | if (res === 'Username invalid' || res === 'Password invalid'){ 537 | this.setState({ 538 | error: res, 539 | username: '', 540 | password: '', 541 | }) 542 | } else { 543 | Actions.feed(); 544 | } 545 | }); 546 | } 547 | 548 | render(){ 549 | return ( 550 | 551 | 552 | {this.state.error} 553 | 558 | 559 | this.setState({username: text})} 564 | /> 565 | this.setState({password: text})} 571 | /> 572 | 579 | 580 | 586 | 587 | 588 | ); 589 | } 590 | } 591 | 592 | export default connect(null, mapDispatchToProps)(Login); 593 | ``` 594 | 595 | Again, we check to make sure that the fields have valid input, and then check the login info against what is in our bucket using our `authenticate` function: 596 | 597 | ``` javascript 598 | export const authenticate = user => dispatch => { 599 | return axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/users/search?metafield_key=username&metafield_value=${user.username}`) 600 | .then(res => res.data) 601 | .then(data => { 602 | console.log('RESPONSE: ', data); 603 | if (data.objects) { 604 | const userData = data.objects[0]; 605 | return { 606 | password: userData.metadata.password, 607 | username: userData.metadata.username, 608 | name: userData.metadata.name, 609 | profilePicture: userData.metadata.profile_picture, 610 | slug: userData.slug, 611 | id: userData._id, 612 | } 613 | } else { 614 | return 'Username invalid'; 615 | } 616 | }) 617 | .then(data => { 618 | if (data === 'Username invalid'){ 619 | return data; 620 | } else if (data.password === user.password){ 621 | dispatch(login({ 622 | name: data.name, 623 | username: data.username, 624 | profilePicture: data.profilePicture, 625 | slug: data.slug, 626 | id: data.id, 627 | })) 628 | } else { 629 | return 'Password invalid'; 630 | } 631 | }) 632 | .catch(error => console.error('Login unsuccessful', error)) 633 | } 634 | ``` 635 | As a side note, we normally wouldn't want to be storing user credentials directly to our database without some kind of encryption, but we'll leave it like this for now as a simple illustration of how we can manage our data with the Cosmic API. 636 | 637 | When users are logged in, they will go directly to the Feed layout, which looks like this: 638 | 639 | ``` javascript 640 | import React, { Component } from 'react'; 641 | import { connect } from 'react-redux'; 642 | import { Actions } from 'react-native-router-flux'; 643 | import { 644 | Container, 645 | Content, 646 | List, 647 | Button, 648 | Icon, 649 | Text, 650 | } from 'native-base'; 651 | 652 | import SinglePost from '../../components/SinglePost'; 653 | import FeedNavbar from '../../components/FeedNavbar'; 654 | import { loadPosts } from '../../redux/reducers/posts'; 655 | import { logoutUser } from '../../redux/reducers/users'; 656 | import styles from './styles'; 657 | 658 | const mapStateToProps = ({ posts }) => ({ posts }); 659 | 660 | const mapDispatchToProps = { loadPosts, logoutUser }; 661 | 662 | const renderPost = (post, index) => ( 663 | 670 | ) 671 | 672 | class Feed extends Component { 673 | componentDidMount(){ 674 | this.props.loadPosts(); 675 | } 676 | 677 | render(){ 678 | const endMsg = this.props.posts.length === 0 ? "There aren't any posts yet!" : "That's all the posts for now!" 679 | 680 | return ( 681 | 682 | 683 | 684 | 685 | { 686 | !!this.props.posts.length && this.props.posts.map(renderPost) 687 | } 688 | 689 | {endMsg} 690 | 691 | 701 | 702 | ); 703 | } 704 | } 705 | 706 | export default connect(mapStateToProps, mapDispatchToProps)(Feed); 707 | ``` 708 | 709 | When the Feed layout mounts, we make a call to the Cosmic API to load all of the posts in our bucket onto our app state. The `loadPosts` function, in our `posts` reducer, looks like this: 710 | 711 | ``` javascript 712 | export const loadPosts = () => dispatch => { 713 | return axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/posts`) 714 | .then(res => res.data.objects ? formatPosts(res.data.objects) : []) 715 | .then(formattedPosts => formattedPosts.sort(postSorter)) 716 | .then(sortedPosts => dispatch(init(sortedPosts))) 717 | .catch(err => console.error(`Could not load posts`, err)); 718 | }; 719 | ``` 720 | 721 | We pull in all of the posts that are in our bucket, format them in a way that will make it easy to get the data that we want, and load them onto state. They are then displayed in the feed. 722 | 723 | From the feed, users can click a button to make a new post. They are then taken to the `NewPost` layout: 724 | 725 | ``` javascript 726 | import React, { Component } from 'react'; 727 | import { connect } from 'react-redux'; 728 | import { 729 | Container, 730 | Content, 731 | Text, 732 | Button, 733 | } from 'native-base'; 734 | import { View } from 'react-native'; 735 | import TextField from '../../components/TextField'; 736 | import styles from './styles'; 737 | 738 | import { createPost } from '../../redux/reducers/posts'; 739 | 740 | const mapStateToProps = state => ({ 741 | user: state.user, 742 | }) 743 | 744 | const mapDispatchToProps = { createPost }; 745 | 746 | class NewPost extends Component { 747 | constructor(){ 748 | super(); 749 | this.state = { 750 | content: '', 751 | error: '', 752 | } 753 | } 754 | onSubmit() { 755 | if (this.state.content){ 756 | this.props.createPost({ 757 | user: this.props.user, 758 | content: this.state.content, 759 | }) 760 | } else { 761 | this.setState({error: 'You have to write something!'}); 762 | } 763 | } 764 | 765 | render(){ 766 | return ( 767 | 768 | 769 | {this.state.error} 770 | 771 | this.setState({content: text})} 776 | /> 777 | 784 | 785 | 786 | 787 | ); 788 | } 789 | } 790 | 791 | export default connect(mapStateToProps, mapDispatchToProps)(NewPost); 792 | ``` 793 | 794 | When they submit their post, we will send it to our bucket: 795 | 796 | ``` javascript 797 | export const createPost = post => dispatch => { 798 | return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/add-object`, { 799 | title: post.user.username + ' post', 800 | type_slug: 'posts', 801 | content: post.content, 802 | metafields: [ 803 | { 804 | type: 'object', 805 | title: 'User', 806 | key: 'user', 807 | object_type: 'users', 808 | value: post.user.id 809 | }, 810 | ] 811 | }) 812 | .then(res => formatPost(res.data, post)) 813 | .then(formattedPost => dispatch(create(formattedPost))) 814 | .then(() => Actions.feed({type: 'popAndReplace'})) 815 | .catch(error => console.error('Post unsuccessful', error)) 816 | } 817 | ``` 818 | 819 | and then redirect back to the feed which will pull in the updated list of posts. Users can also refresh their feed to see new posts and logout from the `Feed`. 820 | 821 | ## TL;DR 822 | 823 | We made a Twitter-like app using React Native which utilized the power of the Cosmic JS API to easily maintain all of the data for our users and posts. We were able to get up and running quickly with a few simple actions that POST and GET our data to/from our Cosmic JS bucket. 824 | --------------------------------------------------------------------------------