DEV Community

Hayk Adamyan
Hayk Adamyan

Posted on

6 2

How to Build a Location-based Twitter Search App with React and Cosmic JS

Background Image

Tweets by location

Hi, In this tutorial, we are going to create an application that filters twitter tweets by location using React and Cosmic JS. We will be using Cosmic JS npm package for the implementation of the basic CRUD system in our application. Let's get started.

TL;DR

View the Demo
Download the GitHub repo

Prerequisites

You will be required to install Node JS and NPM before starting. Make sure you already have them.

Getting Started

Doing everything using the existing git repo

First of all, you have to be sure you have node > 8.x:
As our application uses external APIs, such as Cosmic JS, Gmail API and Twitter API we have to set the environment variables for these APIs.
Note you will have to create API keys in the services mentioned above to put the "KEYS" in the .env file.
Here is how your .env file has to look like.

COSMIC_BUCKET=*****************
COSMIC_READ_KEY==*****************
COSMIC_WRITE_KEY==*****************
GMAPS_API_KEY==*****************
TWITTER_CONSUMER_KEY==*****************
TWITTER_CONSUMER_SECRET_KEY==*****************
TWITTER_ACCESS_TOKEN==*****************
TWITTER_ACCESS_TOKEN_SECRET==*****************
view raw .env hosted with ❤ by GitHub

After setting up the .envfile we have to run the following commands.

$ git clone https://github.com/haykadamyan/tweet-locator.git
$ cd tweet-locator
$ npm install
$ npm run dev
view raw gistfile1.txt hosted with ❤ by GitHub

After the successfull completion of the last command browser window will automatically open, and the package.json will look like this.

{
"name": "react-starter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"axios": "^0.18.0",
"cosmicjs": "^3.2.7",
"dotenv": "^6.2.0",
"express": "^4.16.3",
"lodash": "^4.17.11",
"next": "^7.0.2",
"normalize.css": "^8.0.1",
"prop-types": "^15.6.2",
"query-string": "^6.2.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-google-maps": "^9.4.5",
"whatwg-fetch": "^3.0.0"
},
"devDependencies": {},
"scripts": {
"dev": "node server.js",
"develop": "npm run dev",
"build": "next build",
"start": "NODE_ENV=production node server.js",
"import": "node ./scripts/import.js"
},
"author": "",
"license": "ISC"
}
view raw package.json hosted with ❤ by GitHub

Now the app has to be running on http://localhost:3000

Congratulations!!!

Source code

Server.js

Now it's time to understand how the magic works.
Let's take a look at server.js

const next = require('next');
const express = require('express');
const axios = require('axios');
const qs = require('query-string');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const PORT = process.env.PORT || 3000;
app.prepare().then(() => {
const server = express();
server.get('/tweets', (req, res) => {
const q = req.query.q || 'news';
const params = {
q,
geocode: `${req.query.lat},${req.query.lng},10km`,
include_entities: false,
count: 100,
};
axios({
url: `https://api.twitter.com/1.1/search/tweets.json?${qs.stringify(
params,
)}`,
method: 'GET',
headers: {
Authorization: `Bearer ${req.query.accessToken}`,
'Content-Type': 'application/json',
},
})
.then(items => {
res.json({
error: false,
items: items.data.statuses,
});
})
.catch(() => {
res.json({
error: true,
message: 'Something went wrong',
});
});
});
server.get('*', (req, res) => handle(req, res));
server.listen(PORT, err => {
if (err) throw err;
console.log(`> Ready on http://localhost:${PORT}`);
});
});
view raw server.js hosted with ❤ by GitHub

Nothing special in this code, unless we take a look at the line 14. Here we setup the endpoint for the twitter API requests, and prepare the standart request body.
More about this at Twitter API Documentation

Pages/index.js

pages/index.js is our home page file, where we have our UI, and the requests to the API's we use.

import React from 'react';
import App from '../components/App';
import Flex, { Col } from '../components/Flex';
import Map from '../components/Map';
import TwitterService from '../lib/Twitter';
import fetch from '../lib/cosmic';
class Index extends React.Component {
static async getInitialProps() {
let twitterAccessToken;
try {
twitterAccessToken = await TwitterService.obtainAccessToken();
} catch (error) {
twitterAccessToken = '';
}
return {
twitterAccessToken,
};
}
state = {
q: '',
isLoading: false,
tweets: [],
mapLocation: {
lat: 40.627307,
lng: -73.937884,
},
position: {
lat: 40.627307,
lng: -73.937884,
},
popular: [],
};
componentDidMount() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(position => {
const currentLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
this.setState({
position: currentLocation,
mapLocation: currentLocation,
});
});
}
fetch
.getSearchHistory()
.then(data => {
const x = _.orderBy(data, item => {
return item.metadata.searchCounts;
}, ['desc']);
console.log('--x ape--', x);
this.setState({
popular: data,
});
});
}
search() {
this.setState({
isLoading: true,
});
const q = this.state.q || 'news';
TwitterService.searchTweets({
accessToken: this.props.twitterAccessToken,
...this.state.position,
q,
}).then(tweets => {
fetch.addHistory(q, tweets.length);
this.setState({
tweets,
isLoading: false,
});
});
}
onChange = e => {
this.setState({
[e.target.name]: e.target.value,
});
};
onSearch = e => {
e.preventDefault();
this.search();
};
onChangeLocation = position => {
this.setState(
{
position,
},
() => {
this.search();
},
);
};
onSelect = title => {
this.setState(
{
q: title,
},
() => {
this.search();
},
);
};
render() {
return (
<App>
<Flex>
<Col xs={8}>
<Map
defaultLocation={this.state.mapLocation}
onChangeLocation={this.onChangeLocation}
/>
</Col>
<Col xs={4}>
<div className="content">
<div className="search-bar">
<Flex xs={{ align: 'center', justify: 'space-between' }}>
<Col xs={8}>
<input
type="text"
className="input-search"
name="q"
value={this.state.q}
onChange={this.onChange}
/>
</Col>
<Col>
<button className="btn" onClick={this.onSearch}>
Search
</button>
</Col>
</Flex>
<div className="popular-categories">
<p className="popular-topics">Popular topics</p>
{this.state.popular.map((item, key) => (
<span
key={key}
className="category-item"
onClick={() => this.onSelect(item.title)}
>
{item.title}
</span>
))}
</div>
</div>
<div className="tweet-container">
{this.state.isLoading && <p>Loading...</p>}
{!this.state.isLoading &&
this.state.tweets.map((item, key) => (
<div key={key} className="tweet-item">
<Flex xs={{ align: 'center' }}>
<Col xs={{ right: 10 }}>
<img
className="profile-image"
src={item.user.profile_image_url_https}
alt="Profile"
/>
</Col>
<Col>
<p><a href={`https://twitter.com/${item.user.screen_name}`} target="_blank" rel="noreferrer noopener">{item.user.name}</a></p>
</Col>
</Flex>
<p className="tweet-text">{item.text}</p>
<Flex xs={{ justify: 'flex-end' }}>
<Col>
<a href={`https://twitter.com/statuses/${item.id_str}`} target="_blank" rel="noreferrer noopener">More...</a>
</Col>
</Flex>
</div>
))}
</div>
</div>
</Col>
</Flex>
<div className="footer">
<Flex xs={{ height: '5vh', align: 'center', justify: 'center' }}>
Proudly Powered by &ensp; <a href="https://cosmicjs.com" target="_blank">Cosmic JS</a>
</Flex>
</div>
<style jsx global>{`
body {
font-family: sans-serif;
}
a {
color: #00A4EF;
text-decoration: none;
}
p {
margin: 0;
}
`}</style>
<style jsx>
{`
.content {
padding: 0 15px;
}
.search-bar {
height: 15vh;
border-radius: 10px;
box-shadow: 0 5px 20px 0 rgba(204, 204, 204, 0.5);
padding: 10px 15px;
margin: 10px 0;
word-break: break-all;
}
.popular-categories {
margin-top: 10px;
}
.popular-topics {
margin-bottom: 5px;
color: #8c8c8c;
font-size: 14px;
}
.category-item {
color: #00A4EF;
margin-right: 10px;
cursor: pointer;
}
.tweet-container {
height: 75vh;
background: #fff;
padding: 0 15px;
overflow-y: scroll;
border-radius: 10px;
box-shadow: 0 5px 20px 0 rgba(204, 204, 204, 0.5);
}
.tweet-item {
color: #14171a;
border-bottom: 1px solid #e6ecf0;
word-break: break-all;
margin: 10px 0;
padding-bottom: 10px;
}
.profile-image {
height: 30px;
border-radius: 50%;
border: 1px solid #ccc;
}
.tweet-text {
margin: 5px 0;
}
.input-search {
width: 100%;
box-sizing: border-box;
border: 0;
border-bottom: 1px solid #d3dfef;
font-size: 14px;
letter-spacing: 0.3px;
padding: 14px 20px;
}
.btn {
width: 100%;
box-sizing: border-box;
border: 0;
border-bottom: 1px solid #d3dfef;
font-size: 14px;
letter-spacing: 0.3px;
padding: 14px 20px;
transition: all 0.2s linear;
box-shadow: 0 4px 16px 0 rgba(69, 91, 99, 0.08);
}
.footer {
width: 100%;
background: #B9E7A7;
color: #717171;
}
`}
</style>
</App>
);
}
}
Index.propTypes = {};
export default Index;
view raw pages->index.js hosted with ❤ by GitHub
Important functions
  • onChangeLocation - calls search function when we select a location on the map
  • search - Sends request to the Twitter API and receives the tweets in the choosen location
  • componentDidMount - Loads the most popular search topics through Cosmic JS API
Lib/cosmic.js

lib/comsic.js is the file where our code of relations with Buckets has been hosted.

import Cosmic from 'cosmicjs';
import _ from 'lodash';
const api = Cosmic();
const bucket = api.bucket({
slug: process.env.COSMIC_BUCKET,
read_key: process.env.COSMIC_READ_KEY,
write_key: process.env.COSMIC_WRITE_KEY,
});
function getSearchHistory() {
return bucket
.getObjects({
type: 'searches',
limit: 100,
sort: 'modified_at',
})
.then(item => {
return _.take(_.orderBy(item.objects, i => i.metadata.searchCounts, ['desc']), 10)
});
}
function addHistory(title, totalTweets = 0) {
bucket
.getObject({ slug: _.lowerCase(title) })
.then(data => {
const obj = data.object;
return bucket.editObject({
slug: obj.slug,
metafields: [
{
key: 'searchCounts',
type: 'text',
value: obj.metadata.searchCounts + 1,
},
{
key: 'totalTweets',
type: 'text',
value: totalTweets,
},
],
});
})
.catch(() => {
const params = {
title,
type_slug: 'searches',
content: '',
metafields: [
{
key: 'searchCounts',
type: 'text',
value: 1,
},
{
key: 'totalTweets',
type: 'text',
value: totalTweets,
},
],
options: {
slug_field: false,
},
};
return bucket.addObject(params);
});
}
export default {
getSearchHistory,
addHistory,
};
view raw lib->cosmic.js hosted with ❤ by GitHub
Important functions
  • getSearchHistory - returns the history of the searches that were done
  • addHistory - checks if we have that topic searched before, if yes we make the popularity rank of the topic higher in our db, if not we add it there with the popularity rank 0.
Lib/twitter.js

lib/twitter.js is the file where our code for Twitter API is hosted.

import axios from 'axios';
import qs from 'query-string';
class TwitterService {
static obtainAccessToken() {
const options = {
url: 'https://api.twitter.com/oauth2/token',
method: 'POST',
headers: {
Authorization: `Basic Q1dVeGFwb1FnT2U4WlAzMXpkeXZ1NUdQeTpHZm53dlZPajdhc1BRWFBDekw0ZHNPQmE3Qk1LRHNoOHhCa25MYjE5RWtyTmlaWXlQYg==`,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: qs.stringify({
grant_type: 'client_credentials',
}),
};
return axios(options).then(res => res.data.access_token);
}
static searchTweets(params) {
const options = {
url: `/tweets?${qs.stringify(params)}`,
method: 'GET',
};
return axios(options).then(res => res.data.items);
}
}
export default TwitterService;
view raw lib->Twitter.js hosted with ❤ by GitHub
Important functions
  • searchTweets - sends a search request to the Twitter API

Conclusion

In this tutorial we've learned how to build an app that filters twitter tweets by location using React and Cosmic JS.

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (4)

Collapse
 
dance2die profile image
Sung M. Kim

Hi Hayk.

The links are pointing to "Google.com" & "Github.com", respectively.

# TL;DR
View the Demo
Download the GitHub repo
Collapse
 
haykadamyan profile image
Hayk Adamyan

Thanks for your attention, fixed now :)

Collapse
 
dance2die profile image
Sung M. Kim

Thanks 👍

Collapse
 
carsoncgibbons profile image
Carson Gibbons

Great article, Hayk!

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay