From 0a7f31a1d14f1fe5052499ed22cae4607fb50563 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 19 Oct 2015 14:19:04 -0700 Subject: [PATCH] Add twitter follower button to board --- client/less/main.less | 2 +- common/app/routes/Jobs/components/Jobs.jsx | 16 ++-- .../app/routes/Jobs/components/TwitterBtn.jsx | 33 ++++++++ common/app/routes/Jobs/flux/Actions.js | 20 +++++ common/app/routes/Jobs/flux/Store.js | 4 +- common/utils/jsonp$.js | 77 +++++++++++++++++++ 6 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 common/app/routes/Jobs/components/TwitterBtn.jsx create mode 100644 common/utils/jsonp$.js diff --git a/client/less/main.less b/client/less/main.less index b24a4a35b7..35b5491ae2 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -959,7 +959,6 @@ code { z-index: 20000 !important; } -@import "jobs.less"; //uncomment this to see the dimensions of all elements outlined in red //* { @@ -1070,3 +1069,4 @@ code { } @import "chat.less"; +@import "jobs.less"; diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index d9c7eccdf4..bc995ae9c1 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -3,6 +3,7 @@ import { contain } from 'thundercats-react'; import { Button, Panel, Row, Col } from 'react-bootstrap'; import ListJobs from './List.jsx'; +import TwitterBtn from './TwitterBtn.jsx'; export default contain( { @@ -18,12 +19,18 @@ export default contain( propTypes: { children: PropTypes.element, + numOfFollowers: PropTypes.number, appActions: PropTypes.object, jobActions: PropTypes.object, jobs: PropTypes.array, showModal: PropTypes.bool }, + componentDidMount() { + const { jobActions } = this.props; + jobActions.getFollowers(); + }, + handleJobClick(id) { const { appActions, jobActions } = this.props; if (!id) { @@ -54,6 +61,7 @@ export default contain( render() { const { children, + numOfFollowers, jobs, appActions } = this.props; @@ -88,13 +96,7 @@ export default contain( Post a job: $200 for 30 days + weekly tweets
- - Follow @CamperJobs - +
diff --git a/common/app/routes/Jobs/components/TwitterBtn.jsx b/common/app/routes/Jobs/components/TwitterBtn.jsx new file mode 100644 index 0000000000..fb5f8edf12 --- /dev/null +++ b/common/app/routes/Jobs/components/TwitterBtn.jsx @@ -0,0 +1,33 @@ +import React, { createClass, PropTypes } from 'react'; +import { Button } from 'react-bootstrap'; + +const followLink = 'https://twitter.com/intent/follow?' + + 'ref_src=twsrc%5Etfw&region=follow_link&screen_name=CamperJobs&' + + 'amp;tw_p=followbutton'; + +function commify(count) { + return Number(count).toLocaleString('en'); +} + +export default createClass({ + + displayName: 'FollowButton', + + propTypes: { + count: PropTypes.number + }, + + render() { + const { count } = this.props; + return ( + + ); + } +}); diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index d37f16a7f5..d14d83716d 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,6 +1,7 @@ import { Actions } from 'thundercats'; import store from 'store'; import debugFactory from 'debug'; +import { jsonp$ } from '../../../../utils/jsonp$'; const debug = debugFactory('freecc:jobs:actions'); const assign = Object.assign; @@ -59,6 +60,10 @@ export default Actions({ getSavedForm: null, setForm(form) { return { form }; + }, + getFollowers: null, + setFollowersCount(numOfFollowers) { + return { numOfFollowers }; } }) .refs({ displayName: 'JobActions' }) @@ -113,5 +118,20 @@ export default Actions({ appActions.goTo(goTo); }); }); + + jobActions.getFollowers.subscribe(() => { + const url = 'https://cdn.syndication.twimg.com/widgets/followbutton/' + + 'info.json?lang=en&screen_names=CamperJobs' + + '&callback=JSONPCallback'; + + jsonp$(url) + .map(({ response }) => { + return response[0]['followers_count']; + }) + .subscribe( + count => jobActions.setFollowersCount(count), + err => jobActions.setError(err) + ); + }); return jobActions; }); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index 8f3acd2949..15aa55b770 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -19,7 +19,8 @@ export default Store({ openModal, closeModal, handleForm, - setForm + setForm, + setFollowersCount } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); @@ -27,6 +28,7 @@ export default Store({ register(setter(openModal)); register(setter(closeModal)); register(setter(setForm)); + register(setter(setFollowersCount)); register(transformer(findJob)); register(handleForm); diff --git a/common/utils/jsonp$.js b/common/utils/jsonp$.js new file mode 100644 index 0000000000..99dbbe2eb4 --- /dev/null +++ b/common/utils/jsonp$.js @@ -0,0 +1,77 @@ +import { AnonymousObservable, Disposable } from 'rx'; + +const root = typeof window !== 'undefined' ? window : {}; +const trash = 'document' in root && root.document.createElement('div'); + +function destroy(element) { + trash.appendChild(element); + trash.innerHTML = ''; +} + +export function jsonp$(options) { + let id = 0; + if (typeof options === 'string') { + options = { url: options }; + } + + return new AnonymousObservable(function(o) { + const settings = Object.assign( + {}, + { + jsonp: 'JSONPCallback', + async: true, + jsonpCallback: 'rxjsjsonpCallbackscallback_' + (id++).toString(36) + }, + options + ); + + let script = root.document.createElement('script'); + script.type = 'text/javascript'; + script.async = settings.async; + script.src = settings.url.replace(settings.jsonp, settings.jsonpCallback); + + root[settings.jsonpCallback] = function(data) { + root[settings.jsonpCallback].called = true; + root[settings.jsonpCallback].data = data; + }; + + const handler = function(e) { + if (e.type === 'load' && !root[settings.jsonpCallback].called) { + e = { type: 'error' }; + } + const status = e.type === 'error' ? 400 : 200; + const data = root[settings.jsonpCallback].data; + + if (status === 200) { + o.onNext({ + status: status, + responseType: 'jsonp', + response: data, + originalEvent: e + }); + + o.onCompleted(); + } else { + o.onError({ + type: 'error', + status: status, + originalEvent: e + }); + } + }; + + script.onload = script.onreadystatechanged = script.onerror = handler; + + const head = root.document.getElementsByTagName('head')[0] || + root.document.documentElement; + + head.insertBefore(script, head.firstChild); + + return Disposable.create(() => { + script.onload = script.onreadystatechanged = script.onerror = null; + + destroy(script); + script = null; + }); + }); +}