├── .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 |
12 | )
13 |
--------------------------------------------------------------------------------
/app/components/SmallButton/styles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import colors from '../../config/styles';
3 |
4 | export const styles = StyleSheet.create({
5 | button: {
6 | padding: 10,
7 | backgroundColor: colors.deepBlue,
8 | borderRadius: 5,
9 | width: 100,
10 | },
11 | label: {
12 | textAlign: 'center',
13 | fontSize: 14,
14 | fontWeight: '600',
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/app/layouts/login/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | marginTop: 128,
4 | },
5 | icon: {
6 | fontSize: 96,
7 | margin: 15,
8 | alignSelf: 'center',
9 | },
10 | loginBox: {
11 | margin: 10,
12 | },
13 | button: {
14 | marginTop: 20,
15 | },
16 | signupBtn: {
17 | alignSelf: 'center',
18 | },
19 | signupTxt: {
20 | fontSize: 12,
21 | },
22 | formMsg: {
23 | fontSize: 10,
24 | color: 'red',
25 | alignSelf: 'center',
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/app/redux/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import { composeWithDevTools } from 'redux-devtools-extension'
3 | import { createLogger } from 'redux-logger'
4 | import thunkMiddleware from 'redux-thunk'
5 | import rootReducer from './reducers'
6 |
7 |
8 | const store = createStore(
9 | rootReducer,
10 | composeWithDevTools(
11 | applyMiddleware(
12 | createLogger({collapsed: true}),
13 | thunkMiddleware
14 | )
15 | )
16 | )
17 |
18 | export default store;
19 |
--------------------------------------------------------------------------------
/app/components/TextField/TextField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Item, Label, Input } from 'native-base';
3 | import styles from './styles';
4 |
5 | export default (props) => (
6 | -
7 |
8 |
16 |
17 | )
18 |
--------------------------------------------------------------------------------
/app/layouts/welcome/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | flex: 1,
4 | marginTop: 128,
5 | },
6 | iconBox: {
7 | flex: 1,
8 | alignItems: 'center',
9 | },
10 | icon: {
11 | fontSize: 96,
12 | },
13 | welcome: {
14 | fontFamily: 'Pacifico',
15 | fontSize: 48,
16 | },
17 | buttonContainer: {
18 | flex: 1,
19 | flexDirection: 'column',
20 | alignItems: 'center',
21 | },
22 | or: {
23 | fontFamily: 'Pacifico',
24 | },
25 | button: {
26 | margin: 10,
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Twitter Clone
2 | 
3 | A React Native Twitter clone that uses Cosmic JS for data storage. Register / login and add posts to your feed.
4 |
5 | ## Getting Started
6 | Create a new Bucket after logging in to [Cosmic JS](https://cosmicjs.com).
7 | ```
8 | git clone https://github.com/cosmicjs/react-native-twitter-clone
9 | cd react-native-twitter-clone
10 | yarn
11 | COSMICT_BUCKET=your-bucket-slug yarn start
12 | ````
13 |
--------------------------------------------------------------------------------
/app/layouts/signup/styles.js:
--------------------------------------------------------------------------------
1 | export default {
2 | container: {
3 | marginTop: 64,
4 | },
5 | mar10: {
6 | margin: 10,
7 | },
8 | addPic: {
9 | alignSelf: 'center',
10 | },
11 | uploadButton: {
12 | alignSelf: 'center',
13 | margin: 10,
14 | borderRadius: 100,
15 | },
16 | thumbnail: {
17 | alignSelf: 'center',
18 | margin: 10,
19 | },
20 | formMsg: {
21 | fontSize: 10,
22 | color: 'red',
23 | alignSelf: 'center',
24 | },
25 | loginBtn: {
26 | alignSelf:'center',
27 | padding: 0,
28 | },
29 | loginTxt: {
30 | fontSize: 10,
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/components/SinglePost/SinglePost.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ListItem, Thumbnail, Text, Body } from 'native-base';
3 | import { View } from 'react-native';
4 | import styles from './styles';
5 |
6 | export default (props) => (
7 |
8 |
9 |
10 |
11 | {props.name}
12 | @{props.username}
13 |
14 | {props.content}
15 |
16 |
17 | )
18 |
--------------------------------------------------------------------------------
/app/components/FeedNavbar/FeedNavbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Text,
4 | Button,
5 | Icon,
6 | Header,
7 | Title,
8 | Left,
9 | Right,
10 | Body } from 'native-base';
11 |
12 |
13 | export default (props) => (
14 |
15 |
16 |
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 |
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 |
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 |
--------------------------------------------------------------------------------