├── CHANGELOG.md ├── .gitattributes ├── .bowerrc ├── bower.json ├── src ├── common │ ├── icons │ │ ├── icon-16.png │ │ ├── icon-19.png │ │ ├── icon-38.png │ │ └── icon-128.png │ ├── loader │ │ ├── loader.png │ │ ├── loader@2x.png │ │ └── _main.scss │ ├── constants │ │ └── index.js │ ├── dispatcher │ │ └── index.js │ ├── screens │ │ └── _main.scss │ ├── analytics │ │ ├── NullAnalytics.js │ │ ├── index.js │ │ ├── __tests__ │ │ │ └── Tracker-test.js │ │ └── tracker.js │ ├── img │ │ └── _main.scss │ ├── render │ │ ├── index.js │ │ └── __tests__ │ │ │ └── index-test.js │ ├── google-analytics │ │ ├── __tests__ │ │ │ └── index-test.js │ │ └── index.js │ ├── settings │ │ ├── __tests__ │ │ │ └── index-test.js │ │ └── index.js │ ├── actions │ │ ├── __tests__ │ │ │ └── PostActions-test.js │ │ └── PostActions.js │ ├── stores │ │ ├── __tests__ │ │ │ └── PostStore-test.js │ │ └── PostStore.js │ ├── api │ │ └── index.js │ └── product-hunt │ │ └── index.js ├── apps │ ├── tabs │ │ ├── assets │ │ │ ├── arrow_up.svg │ │ │ ├── comment.svg │ │ │ └── logo.svg │ │ ├── components │ │ │ ├── __tests__ │ │ │ │ ├── Logo.react-test.js │ │ │ │ ├── PostGroup.react-test.js │ │ │ │ ├── DefaultTab.react-test.js │ │ │ │ └── Post.react-test.js │ │ │ ├── Logo.react.js │ │ │ ├── PostGroup.react.js │ │ │ ├── Post.react.js │ │ │ ├── InfiniteScroll.react.js │ │ │ └── DefaultTab.react.js │ │ ├── util │ │ │ ├── groupByDay.js │ │ │ ├── __tests__ │ │ │ │ ├── groupByDay-test.js │ │ │ │ ├── sliceWithRest-test.js │ │ │ │ └── getDay-test.js │ │ │ ├── getDay.js │ │ │ ├── buildPost.js │ │ │ └── sliceWithRest.js │ │ ├── main.html │ │ ├── main.js │ │ └── main.scss │ ├── popup │ │ ├── main.html │ │ ├── components │ │ │ ├── __tests__ │ │ │ │ ├── Popup.react-test.js │ │ │ │ └── Tab.react-test.js │ │ │ ├── popup.react.js │ │ │ └── tab.react.js │ │ ├── main.js │ │ └── main.scss │ └── background │ │ ├── buildUrl.js │ │ ├── xframe.js │ │ └── main.js ├── _locales │ └── en │ │ └── messages.json └── manifest.json ├── .gitignore ├── .env.assert ├── test ├── setup.js └── popup.js ├── .env.test ├── LICENSE ├── jest ├── setup.js └── env.js ├── .env.example ├── package.json ├── README.md ├── CONTRIBUTING.md └── Gulpfile.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "build/vendor/" 3 | } 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "producthunt-chrome", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": {} 6 | } 7 | -------------------------------------------------------------------------------- /src/common/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/producthunt/producthunt-chrome-extension/HEAD/src/common/icons/icon-16.png -------------------------------------------------------------------------------- /src/common/icons/icon-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/producthunt/producthunt-chrome-extension/HEAD/src/common/icons/icon-19.png -------------------------------------------------------------------------------- /src/common/icons/icon-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/producthunt/producthunt-chrome-extension/HEAD/src/common/icons/icon-38.png -------------------------------------------------------------------------------- /src/common/loader/loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/producthunt/producthunt-chrome-extension/HEAD/src/common/loader/loader.png -------------------------------------------------------------------------------- /src/common/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/producthunt/producthunt-chrome-extension/HEAD/src/common/icons/icon-128.png -------------------------------------------------------------------------------- /src/common/loader/loader@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/producthunt/producthunt-chrome-extension/HEAD/src/common/loader/loader@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tmp/ 3 | /build/ 4 | /dist/ 5 | /.env 6 | /.env.staging 7 | /.env.production 8 | /dev/ 9 | *.pem 10 | -------------------------------------------------------------------------------- /.env.assert: -------------------------------------------------------------------------------- 1 | POST_SEARCH_URL 2 | SEARCH_URL 3 | API_BASE_URL 4 | OAUTH_KEY 5 | OAUTH_SECRET 6 | ANALYTICS_KEY 7 | GA_ID 8 | PRODUCTS_CACHE_KEY 9 | EXTENSION_ID 10 | -------------------------------------------------------------------------------- /src/apps/tabs/assets/arrow_up.svg: -------------------------------------------------------------------------------- 1 | Rectangle-24 -------------------------------------------------------------------------------- /src/common/constants/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants. 3 | */ 4 | 5 | const consts = { 6 | RECEIVE_POST: 'RECEIVE_POST', 7 | RECEIVE_POSTS: 'RECEIVE_POSTS' 8 | }; 9 | 10 | /** 11 | * Export `consts`. 12 | */ 13 | 14 | module.exports = consts; 15 | -------------------------------------------------------------------------------- /src/apps/popup/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product Hunt 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/apps/popup/components/__tests__/Popup.react-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('Popup', function() { 4 | let Popup = require('../Popup.react'); 5 | let React = require('react'); 6 | 7 | it('renders a tab', function() { 8 | expect().toRender('iframe'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/apps/tabs/components/__tests__/Logo.react-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | import React from 'react'; 4 | import Logo from '../Logo.react'; 5 | 6 | describe('Logo', function() { 7 | it('renders the Product Hunt header', function() { 8 | expect().toRender('Product Hunt'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/common/dispatcher/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let Dispatcher = require('flux').Dispatcher; 6 | 7 | /** 8 | * Application Dispatcher. 9 | */ 10 | 11 | let AppDispatcher = new Dispatcher; 12 | 13 | /** 14 | * Export `AppDispatcher`. 15 | */ 16 | 17 | module.exports = AppDispatcher; 18 | -------------------------------------------------------------------------------- /src/apps/tabs/assets/comment.svg: -------------------------------------------------------------------------------- 1 | Oval-28 -------------------------------------------------------------------------------- /src/apps/background/buildUrl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build search URL. 3 | * 4 | * @param {String} base url 5 | * @param {String} query 6 | * @returns {String} 7 | * @private 8 | */ 9 | 10 | function buildUrl(baseUrl, query) { 11 | return baseUrl.replace('{query}', encodeURI(query)); 12 | } 13 | 14 | /** 15 | * Export `buildUrl`. 16 | */ 17 | 18 | module.exports = buildUrl; 19 | -------------------------------------------------------------------------------- /src/apps/tabs/util/groupByDay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Group items by `day`. 3 | * 4 | * @param {Array} items 5 | * @returns {Object} 6 | * @public 7 | */ 8 | 9 | export default function groupByDay(items) { 10 | return items.reduce(function(groups, item) { 11 | groups[item.day] = groups[item.day] || []; 12 | groups[item.day].push(item); 13 | return groups; 14 | }, {}); 15 | } 16 | -------------------------------------------------------------------------------- /src/apps/tabs/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Product Hunt: The best new products, every day 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/common/screens/_main.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | $small-screen-breakpoint: 768px; 4 | $large-screen-breakpoint: 1200px; 5 | 6 | // Mixins 7 | 8 | @mixin medium-screen { 9 | @media (min-width: $small-screen-breakpoint + 1px) and (max-width: $large-screen-breakpoint - 1px) { @content; } 10 | } 11 | 12 | @mixin small-screen { 13 | @media (max-width: $small-screen-breakpoint) { @content; } 14 | } 15 | -------------------------------------------------------------------------------- /src/apps/tabs/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/apps/popup/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let React = require('react'); 6 | let renderComponent = require('../../common/render'); 7 | let Popup = require('./components/Popup.react'); 8 | 9 | // Do not render immediately, because Chrome 10 | // will wait for the entire page to load (incl. the iframe) in order 11 | // to show the popup. 12 | setTimeout(function() { 13 | renderComponent(); 14 | }, 0); 15 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Product Hunt", 4 | "description": "The name of the application" 5 | }, 6 | "title": { 7 | "message": "Product Hunt: The best new products, every day", 8 | "description": "Default title" 9 | }, 10 | "appDescription": { 11 | "message": "Product Hunt brings you the best new products, every day.", 12 | "description": "The description of the application" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/apps/tabs/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | import React from 'react'; 6 | import DefaultTab from './components/DefaultTab.react'; 7 | import renderComponent from '../../common/render'; 8 | import loadGoogleAnalytics from '../../common/google-analytics'; 9 | 10 | /** 11 | * Constants. 12 | */ 13 | 14 | const GA_ID = process.env.GA_ID; 15 | 16 | loadGoogleAnalytics(GA_ID); 17 | renderComponent(, document.getElementById('main')); 18 | -------------------------------------------------------------------------------- /src/common/analytics/NullAnalytics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let debug = require('debug')('ph:analytics:null-analytics'); 6 | 7 | /** 8 | * Null Analytics. 9 | */ 10 | 11 | class NullAnalytics { 12 | 13 | /** 14 | * Noop track. 15 | * 16 | * @public 17 | */ 18 | 19 | track() { 20 | debug('track %j', arguments); 21 | } 22 | } 23 | 24 | /** 25 | * Export `NullAnalytics`. 26 | */ 27 | 28 | module.exports = NullAnalytics; 29 | -------------------------------------------------------------------------------- /src/apps/tabs/util/__tests__/groupByDay-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('groupByDay', function() { 4 | let groupByDay = require('../groupByDay'); 5 | 6 | it('returns the items grouped by day', function() { 7 | let item1 = { foo: 'bar', day: '1' }; 8 | let item2 = { foo: 'bar', day: '2' }; 9 | let items = [item1, item2]; 10 | let expected = { '1': [item1], '2': [item2] }; 11 | 12 | expect(groupByDay(items)).toEqual(expected); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/apps/popup/components/__tests__/Tab.react-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('Tab', function() { 4 | let Tab = require('../Tab.react'); 5 | let React = require('react'); 6 | let url = 'http://example.com'; 7 | 8 | it('renders the given page', function() { 9 | expect().toRender(`iframe src="${url}"`); 10 | }); 11 | 12 | it('renders a loader', function() { 13 | expect().toRender('
16 | * ``` 17 | */ 18 | 19 | export default function Logo() { 20 | return ( 21 | 22 | Product Hunt 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/apps/tabs/components/__tests__/PostGroup.react-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | import PostGroup from '../PostGroup.react'; 4 | import React from 'react'; 5 | import buildPost from '../../util/buildPost'; 6 | 7 | describe('PostGroup', function() { 8 | let post = buildPost(); 9 | 10 | it('renders the group name', function() { 11 | expect().toRender('Thursday'); 12 | }); 13 | 14 | it('renders the post name', function() { 15 | expect().toRender(post.name); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/common/img/_main.scss: -------------------------------------------------------------------------------- 1 | @mixin img($width: 300px, $height: 300px, $name: "assets", $extension: ".png") { 2 | $img: $name + $extension; 3 | $x2img: $name + "@2x" + $extension; 4 | 5 | background-image: image-url($img); 6 | background-repeat: no-repeat; 7 | @media (min--moz-device-pixel-ratio: 1.3), 8 | (-o-min-device-pixel-ratio: 2.6/2), 9 | (-webkit-min-device-pixel-ratio: 1.3), 10 | (min-device-pixel-ratio: 1.3), 11 | (min-resolution: 1.3dppx) { 12 | background-image: image-url($x2img); 13 | background-size: $width; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | var chai = require('chai'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | var envc = require('envc')({ nodeenv: 'test' }); 8 | var path = require('path'); 9 | 10 | // setup chai and wd 11 | global.wd = require('wd'); 12 | 13 | chaiAsPromised.transferPromiseness = wd.transferPromiseness; 14 | chai.use(chaiAsPromised); 15 | chai.should(); 16 | 17 | // global configs 18 | global.TEST = { 19 | path: path.join(__dirname, '..', 'build'), 20 | url: 'chrome-extension://' + process.env.EXTENSION_ID 21 | }; 22 | -------------------------------------------------------------------------------- /src/common/render/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let ReactDom = require('react-dom'); 6 | 7 | /** 8 | * Render React `component`. 9 | * 10 | * @param {Object} component 11 | * @param {DOMElement} container (optional) 12 | * @public 13 | */ 14 | 15 | function render(component, el) { 16 | if (!el) { 17 | el = document.createElement('div'); 18 | document.body.insertBefore(el, document.body.firstChild) 19 | } 20 | 21 | ReactDom.render(component, el); 22 | } 23 | 24 | /** 25 | * Export `render`. 26 | */ 27 | 28 | module.exports = render; 29 | -------------------------------------------------------------------------------- /src/apps/tabs/util/sliceWithRest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Smart slicing: 3 | * 4 | * @example 5 | * 6 | * sliceWithRest([1, 2], 1) -> [[1, 2], []] 7 | * sliceWithRest([1, 2, 3], 1) -> [[1], [2, 3]] 8 | * sliceWithRest([1, 2, 3, 4], 1) -> [[1], [2, 3, 4]] 9 | * 10 | * @param {Array} items 11 | * @param {number} limit 12 | * @returns {[Array, Array]} 13 | * @public 14 | */ 15 | 16 | 17 | export default function sliceWithRest(array, limit) { 18 | if (array.length <= limit + 1) { 19 | return [array, []]; 20 | } 21 | 22 | return [ 23 | array.slice(0, limit), 24 | array.slice(limit), 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /src/apps/tabs/components/__tests__/DefaultTab.react-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | import DefaultTab from '../DefaultTab.react'; 4 | import React from 'react'; 5 | import TestUtils from 'react-addons-test-utils'; 6 | import buildPost from '../../util/buildPost'; 7 | 8 | describe('DefaultTab', function() { 9 | const PostStore = load('/common/stores/PostStore'); 10 | 11 | it('listens for post change events', function() { 12 | let post = buildPost(); 13 | let component = TestUtils.renderIntoDocument(); 14 | PostStore.setData([post]); 15 | PostStore.emitChange(); 16 | expect(component).toRender(post.name); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/apps/tabs/util/__tests__/sliceWithRest-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | import call from '../sliceWithRest'; 4 | 5 | describe('groupByDay', function() { 6 | it('returns all items when their count is less than limit', function() { 7 | let result = call([1], 10); 8 | 9 | expect(result).toEqual([[1], []]); 10 | }); 11 | 12 | it('returns all items when their count + 1 is the limit', function() { 13 | let result = call([1, 2], 1); 14 | 15 | expect(result).toEqual([[1, 2], []]); 16 | }); 17 | 18 | it('splits items after a limit', function() { 19 | let result = call([1, 2, 3], 1); 20 | 21 | expect(result).toEqual([[1], [2, 3]]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/common/render/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('render', function() { 4 | let React = require('react'); 5 | let render = require('../'); 6 | let Dummy = React.createClass({ 7 | render() { 8 | return

Foo

; 9 | } 10 | }); 11 | 12 | it('renders a react component into the body', function() { 13 | render(); 14 | expect(document.querySelector('h1')).toBeTruthy(); 15 | }); 16 | 17 | 18 | it('renders a react component into supplied element', function() { 19 | let container = document.createElement('div'); 20 | render(, container); 21 | expect(container.querySelector('h1')).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | EXTENSION_ID="pinaciaaafilededpcmmpfeakplinncd" 2 | POPUP_URL="http://example.com/" 3 | SEARCH_URL="http://example.com/" 4 | 5 | API_BASE_URL="https://api.producthunt.com" 6 | 7 | OAUTH_KEY="7e5dab6037a254329d6be6b2ea6372d3b6328866f81f78e406f1a620075c54fe" 8 | OAUTH_SECRET="7dedcd8b9f137f5a62fe247f049dc3b4f14d832a1895b2dd2433bb30c2974210" 9 | 10 | PRODUCT_HUNT_HOST="www.producthunt.com" 11 | 12 | PRODUCT_BAR_ID="__phc-bar" 13 | PRODUCT_BAR_HEIGHT="50px" 14 | 15 | BODY_CLASS="__phc-body" 16 | OVERLAY_BODY_CLASS='__phc-no-scroll'; 17 | 18 | FB_APP_ID="1467820943460899" 19 | TWITTER_VIA="producthunt" 20 | 21 | PRODUCTS_CACHE_KEY="ph.chrome.products" 22 | 23 | BAR_DISABLED_KEY="barDisabled" 24 | TAB_DISABLED_KEY="tabDisabled" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, PRODUCT HUNT, INC. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /src/apps/background/xframe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove ProductHunt's X-frame header. 3 | * 4 | * Temporary workaround, because the API responses do not include a link to the widgets 5 | * page. Likely we will just modify the API URL on the fly, instead of removing the 6 | * x-frame option, but that will do too. 7 | */ 8 | 9 | chrome.webRequest.onHeadersReceived.addListener(function(details) { 10 | for (var i = 0; i < details.responseHeaders.length; ++i) { 11 | if (details.responseHeaders[i].name.toLowerCase() == 'x-frame-options') { 12 | details.responseHeaders.splice(i, 1); 13 | return { responseHeaders: details.responseHeaders }; 14 | } 15 | } 16 | }, { urls: ['*://www.producthunt.com/*'] }, ['blocking', 'responseHeaders']); 17 | -------------------------------------------------------------------------------- /src/common/google-analytics/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('Google Analytics', function() { 4 | let loadGa = require('../'); 5 | 6 | describe('without key', function() { 7 | it('does not load Google Analytics', function() { 8 | loadGa(); 9 | expect(document.querySelector('script')).toBeFalsy(); 10 | }); 11 | }); 12 | 13 | describe('with key', function() { 14 | beforeEach(function() { 15 | document.body.appendChild(document.createElement('script')); 16 | }); 17 | 18 | it('loads Google Analytics', function() { 19 | loadGa('key'); 20 | expect(document.querySelector('script').src).toEqual('https://ssl.google-analytics.com/ga.js'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/apps/tabs/util/__tests__/getDay-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('getDay', function() { 4 | let moment = require('moment'); 5 | let getDay = require('../getDay'); 6 | 7 | it('returns today if the supplied date is today', function() { 8 | let date = moment(); 9 | expect(getDay(date)).toEqual('Today'); 10 | }); 11 | 12 | it('returns yesterday if the supplied date is yesetday', function() { 13 | let date = moment().subtract(1, 'day'); 14 | expect(getDay(date)).toEqual('Yesterday'); 15 | }); 16 | 17 | it('returns the actual day of the week if the date is not yesterday or tomorrow', function() { 18 | let date = moment(new Date('2015-03-01')); 19 | expect(getDay(date)).toEqual('Sunday'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/common/settings/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('settings', function() { 4 | let settings = require('../'); 5 | 6 | afterEach(function() { 7 | chrome.storage.sync.get = function(){}; 8 | }); 9 | 10 | it('gets a key from the chrome storage', function() { 11 | chrome.storage.sync.get = function(items, cb) { 12 | expect(items).toEqual({ foo: false }); 13 | }; 14 | 15 | settings.get('foo'); 16 | }); 17 | 18 | it('yields the value returned by the chrome storage', function() { 19 | chrome.storage.sync.get = function(items, cb) { 20 | cb({ foo: 'bar' }); 21 | }; 22 | 23 | settings.get('foo', function(value) { 24 | expect(value).toEqual('bar'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/common/actions/__tests__/PostActions-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | jest.mock('../../dispatcher'); 3 | 4 | var AppDispatcher = require('../../dispatcher'); 5 | 6 | describe('PostActions', function() { 7 | let PostActions = require('../PostActions'); 8 | 9 | describe('#receivePost', function() { 10 | it('dispatches a new action', function() { 11 | var data = { foo: 'bar' }; 12 | PostActions.receivePost(data); 13 | expect(AppDispatcher.dispatch.mock.calls[0][0].action.data).toEqual(data); 14 | }); 15 | }); 16 | 17 | describe('#receivePosts', function() { 18 | it('dispatches a new action', function() { 19 | var data = { foo: 'bar' }; 20 | PostActions.receivePosts(data); 21 | expect(AppDispatcher.dispatch.mock.calls[0][0].action.data).toEqual(data); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /jest/setup.js: -------------------------------------------------------------------------------- 1 | const ReactDOM = require('react-dom'); 2 | const ReactDOMServer = require('react-dom/server'); 3 | const TestUtils = require('react-addons-test-utils'); 4 | 5 | jasmine.addMatchers({ 6 | toRender: function() { 7 | function renderComponent(actual) { 8 | if (TestUtils.isElement(actual)) { 9 | return ReactDOMServer.renderToString(actual); 10 | } else { 11 | return ReactDOM.findDOMNode(actual).innerHTML; 12 | } 13 | } 14 | 15 | return { 16 | compare: function(actual, expected) { 17 | const html = renderComponent(actual); 18 | const pass = !!html.match(expected); 19 | 20 | return { 21 | pass: pass, 22 | message: pass ? '' : `Expected ${ html } to match "${ expected }", but it didn't`, 23 | }; 24 | } 25 | }; 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/common/analytics/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let NullAnalytics = require('./NullAnalytics'); 6 | let Tracker = require('./Tracker'); 7 | 8 | /** 9 | * Constants. 10 | */ 11 | 12 | const ANALYTICS_KEY = process.env.ANALYTICS_KEY; 13 | 14 | /** 15 | * Locals. 16 | * 17 | * Note(andreasklinger): window.ProductHuntAnalytics gets set by a custom built of the analytics.js 18 | * To recreate this use their make script - it offers a options to set the variable name. 19 | */ 20 | 21 | let enableAnalytics = window.ProductHuntAnalytics && ANALYTICS_KEY; 22 | let ProductHuntAnalytics = enableAnalytics ? window.ProductHuntAnalytics : NullAnalytics; 23 | let analytics = new ProductHuntAnalytics(process.env.ANALYTICS_KEY); 24 | 25 | /** 26 | * Export a new `Tracker`. 27 | */ 28 | 29 | module.exports = new Tracker(analytics); 30 | -------------------------------------------------------------------------------- /src/apps/background/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modifiers. 3 | */ 4 | 5 | require('./xframe'); 6 | 7 | /** 8 | * Dependencies. 9 | */ 10 | 11 | let analytics = require('../../common/analytics'); 12 | let buildUrl = require('./buildUrl'); 13 | 14 | /** 15 | * Constants. 16 | */ 17 | 18 | const SEARCH_URL = process.env.POST_SEARCH_URL; 19 | 20 | /** 21 | * Register omnibox "enter" event listner 22 | */ 23 | 24 | chrome.omnibox.onInputEntered.addListener(function(query) { 25 | if (!query) return; 26 | 27 | chrome.tabs.getSelected(null, function(tab) { 28 | let url = buildUrl(SEARCH_URL, query); 29 | chrome.tabs.update(tab.id, { url: url }); 30 | }); 31 | }); 32 | 33 | /** 34 | * Track product bar clicks. 35 | */ 36 | 37 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 38 | analytics.clickBar(request); 39 | }); 40 | -------------------------------------------------------------------------------- /src/common/google-analytics/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let debug = require('debug')('ph:google-analytics'); 6 | 7 | /** 8 | * Load Google Analytics. 9 | * 10 | * @param {String} key 11 | * @public 12 | */ 13 | 14 | function loadGa(key) { 15 | if (!key) { 16 | debug('no key - bail out'); 17 | return; 18 | } 19 | 20 | let _gaq = window._gaq = _gaq || []; 21 | _gaq.push(['_setAccount', key]); 22 | _gaq.push(['_trackPageview']); 23 | 24 | let ga = document.createElement('script'); 25 | ga.type = 'text/javascript'; 26 | ga.async = true; 27 | ga.src = 'https://ssl.google-analytics.com/ga.js'; 28 | 29 | let s = document.getElementsByTagName('script')[0]; 30 | s.parentNode.insertBefore(ga, s); 31 | 32 | debug('inserted'); 33 | } 34 | 35 | /** 36 | * Export `loadGa`. 37 | */ 38 | 39 | module.exports = loadGa; 40 | -------------------------------------------------------------------------------- /src/common/stores/__tests__/PostStore-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('PostStore', function() { 4 | let PostStore = require('../PostStore'); 5 | 6 | afterEach(function() { 7 | PostStore.reset(); 8 | }); 9 | 10 | it('sets/returns a post', function() { 11 | let post = { foo: 'bar' }; 12 | PostStore.setData(post); 13 | expect(PostStore.getPost()).toEqual(post); 14 | }); 15 | 16 | it('sets/returns an array of posts', function() { 17 | let post = { foo: 'bar' }; 18 | PostStore.setData([post]); 19 | expect(PostStore.getPosts()).toEqual([post]); 20 | }); 21 | 22 | it('emits change events', function() { 23 | var cb = jest.genMockFn(); 24 | 25 | PostStore.addChangeListener(cb); 26 | PostStore.emitChange(); 27 | PostStore.removeChangeListener(cb); 28 | 29 | expect(cb).toBeCalled(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/popup.js: -------------------------------------------------------------------------------- 1 | describe('Popup App', function() { 2 | var browser = null; 3 | 4 | before(function() { 5 | browser = wd.promiseChainRemote(); 6 | 7 | return browser.init({ 8 | browserName: 'chrome', 9 | chromeOptions: { 10 | args: ['--load-extension=' + TEST.path] 11 | } 12 | }); 13 | }); 14 | 15 | beforeEach(function() { 16 | return browser.get(TEST.url + '/apps/popup/main.html'); 17 | }); 18 | 19 | after(function() { 20 | return browser.quit(); 21 | }); 22 | 23 | it('has a proper title', function() { 24 | return browser 25 | .title().should.become('Product Hunt'); 26 | }); 27 | 28 | it('loads the specified URL', function() { 29 | return browser 30 | .elementByTagName('iframe') 31 | .getAttribute('src').should.eventually.include('example.com'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/apps/popup/main.scss: -------------------------------------------------------------------------------- 1 | @import "bourbon"; 2 | @import "common/loader/main"; 3 | 4 | // Variables 5 | 6 | $width: 550px; 7 | $height: 550px; 8 | 9 | // Definitions 10 | 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | body { 17 | font-family: 'proxima-nova', 'Proxima Nova', sans-serif; 18 | } 19 | 20 | body, 21 | iframe { 22 | width: $width; 23 | height: $height; 24 | } 25 | 26 | iframe { 27 | border: none; 28 | } 29 | 30 | .loader { 31 | @include loader; 32 | } 33 | 34 | header { 35 | background: #534540; 36 | color: #C4BFBD; 37 | height: 54px; 38 | line-height: 54px; 39 | 40 | ul { 41 | list-style: none; 42 | 43 | li { 44 | cursor: pointer; 45 | font-size: 16px; 46 | float: left; 47 | text-align: center; 48 | width: 50%; 49 | 50 | &:hover, &.current { 51 | background: #fff; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/common/loader/_main.scss: -------------------------------------------------------------------------------- 1 | @import "common/img/main"; 2 | 3 | @mixin loader { 4 | min-height: 40px; 5 | min-width: 40px; 6 | 7 | opacity: 1; 8 | 9 | &.m-hide { 10 | opacity: 0; 11 | pointer-events: none; 12 | } 13 | 14 | &:before { 15 | content: ''; 16 | display: block; 17 | height: 40px; 18 | width: 40px; 19 | @include img(40px, 40px, "common/loader/loader"); 20 | @include animation-name(rotate); 21 | @include animation-duration(1.2s); 22 | @include animation-iteration-count(infinite); 23 | @include animation-timing-function(linear); 24 | @include transition(opacity .1s ease); 25 | 26 | // centering 27 | position: absolute; 28 | left: 50%; 29 | top: 50%; 30 | margin-left: -20px; 31 | margin-top: -20px; 32 | } 33 | } 34 | 35 | @include keyframes(rotate) { 36 | from { 37 | @include transform(rotate(0deg)); 38 | } 39 | to { 40 | @include transform(rotate(360deg)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/apps/popup/components/popup.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let React = require('react'); 6 | let debug = require('debug')('ph:popup:popup'); 7 | let Tab = require('./Tab.react'); 8 | 9 | /** 10 | * Constants. 11 | */ 12 | 13 | const SEARCH_URL = process.env.SEARCH_URL; 14 | 15 | /** 16 | * Popup Component. 17 | * 18 | * Renders the popup. 19 | * 20 | * Usage: 21 | * 22 | * ```js 23 | * 24 | * ``` 25 | * 26 | * State: 27 | * 28 | * - url: Popup URL address 29 | * 30 | * @class 31 | */ 32 | 33 | let Popup = React.createClass({ 34 | 35 | /** 36 | * Return initial state. 37 | * 38 | * @returns {Object} 39 | */ 40 | 41 | getInitialState() { 42 | return { url: SEARCH_URL }; 43 | }, 44 | 45 | /** 46 | * Render the view. 47 | */ 48 | 49 | render() { 50 | return ( 51 |
52 | 53 |
54 | ); 55 | } 56 | }); 57 | 58 | /** 59 | * Export `Popup`. 60 | */ 61 | 62 | module.exports = Popup; 63 | -------------------------------------------------------------------------------- /src/apps/tabs/components/__tests__/Post.react-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | import Post from '../Post.react'; 4 | import React from 'react'; 5 | import buildPost from '../../util/buildPost'; 6 | 7 | describe('Post', function() { 8 | let post = buildPost(); 9 | 10 | it('renders the votes count', function() { 11 | expect().toRender(post.votes_count); 12 | }); 13 | 14 | it('renders the comments count', function() { 15 | expect().toRender(post.comments_count); 16 | }); 17 | 18 | it('renders the post name', function() { 19 | expect().toRender(post.name); 20 | }); 21 | 22 | it('renders the post tagline', function() { 23 | expect().toRender(post.tagline); 24 | }); 25 | 26 | it('renders the screenshot_url', function() { 27 | expect().toRender(post.screenshot_url['300px']); 28 | }); 29 | 30 | it('renders the discussion_url', function() { 31 | expect().toRender(post.discussion_url); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/common/actions/PostActions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let debug = require('debug')('ph:actions:post'); 6 | let AppDispatcher = require('../dispatcher'); 7 | let PostConstants = require('../constants'); 8 | 9 | /** 10 | * Post Actions. 11 | */ 12 | 13 | let PostActions = { 14 | 15 | /** 16 | * Handle the receive post action. 17 | * 18 | * @param {Object} data 19 | * @public 20 | */ 21 | 22 | receivePost(data) { 23 | this._dispatch(PostConstants.RECEIVE_POST, data); 24 | }, 25 | 26 | /** 27 | * Handle the receive posts action. 28 | * 29 | * @param {Object} data 30 | * @public 31 | */ 32 | 33 | receivePosts(data) { 34 | this._dispatch(PostConstants.RECEIVE_POSTS, data); 35 | }, 36 | 37 | /** 38 | * Dispatch action with `type` and `data`. 39 | * 40 | * @param {String} type 41 | * @param {Mixed} data 42 | * @private 43 | */ 44 | 45 | _dispatch(type, data) { 46 | debug('dispatching %s', type); 47 | AppDispatcher.dispatch({ 48 | action: { actionType: type, data: data } 49 | }); 50 | } 51 | }; 52 | 53 | /** 54 | * Export `PostActions`. 55 | */ 56 | 57 | module.exports = PostActions; 58 | -------------------------------------------------------------------------------- /src/apps/tabs/components/PostGroup.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | import React from 'react'; 6 | import moment from 'moment'; 7 | import Post from './Post.react'; 8 | import getDay from '../util/getDay'; 9 | import groupByDay from '../util/groupByDay'; 10 | 11 | /** 12 | * Post Group component. 13 | * 14 | * Renders the posts grouped by date. 15 | * 16 | * Usage: 17 | * 18 | * ```js 19 | * 20 | * ``` 21 | */ 22 | 23 | export default function PostGroup({ posts }) { 24 | let groups = groupByDay(posts); 25 | let out = Object.keys(groups).map(function(day) { 26 | let date = moment(new Date(day)); 27 | let humanDay = getDay(date); 28 | let monthDay = date.format('MMMM Do'); 29 | 30 | return ( 31 |
32 |

33 | {monthDay} 34 | {humanDay} 35 |

36 |
37 | {groups[day].map((post) => )} 38 |
39 |
40 | ); 41 | }); 42 | 43 | return ( 44 |
{out}
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /jest/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | var path = require('path'); 6 | var envc = require('envc')({ nodeenv: 'test' }); 7 | 8 | /** 9 | * Project src directory. 10 | */ 11 | 12 | var srcDir = path.join(__dirname, '..', 'src'); 13 | 14 | /** 15 | * Require wrapper. 16 | * 17 | * @param {String} filename 18 | * @returns {Mixed} 19 | * @public 20 | * @global 21 | */ 22 | 23 | function load(filename) { 24 | return require(filePath(filename)); 25 | } 26 | 27 | /** 28 | * Return path to `file`. 29 | * 30 | * @param {String} path from the src of the project 31 | * @returns {String} 32 | * @public 33 | * @global 34 | */ 35 | 36 | function filePath(file) { 37 | return path.join(srcDir, file); 38 | } 39 | 40 | /** 41 | * Expose `load` globally. 42 | */ 43 | 44 | global.load = load; 45 | 46 | /** 47 | * Expose `filePath` globally. 48 | */ 49 | 50 | global.filePath = filePath; 51 | 52 | /** 53 | * Expose fake `chrome` object. 54 | */ 55 | 56 | global.chrome = window.chrome = { 57 | extension: { 58 | getURL: function() {} 59 | }, 60 | runtime: { 61 | sendMessage: function() {} 62 | }, 63 | storage: { 64 | sync: { 65 | get: function() {}, 66 | set: function() {} 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #------------------------------------ 2 | # 1. URLs 3 | #------------------------------------ 4 | 5 | # Google Chrome Omnibox: Search URL 6 | POST_SEARCH_URL="https://www.producthunt.com/#!/s/posts/{query}" 7 | 8 | # Popup: Post search URL 9 | SEARCH_URL="https://www.producthunt.com/widgets/search#search" 10 | 11 | #------------------------------------ 12 | # 2. API 13 | #------------------------------------ 14 | 15 | # API base URL address 16 | API_BASE_URL="https://api.producthunt.com" 17 | 18 | # API OAuth key 19 | OAUTH_KEY="7e5dab6037a254329d6be6b2ea6372d3b6328866f81f78e406f1a620075c54fe" 20 | 21 | # API OAuth secret 22 | OAUTH_SECRET="7dedcd8b9f137f5a62fe247f049dc3b4f14d832a1895b2dd2433bb30c2974210" 23 | 24 | #------------------------------------ 25 | # 3. Services 26 | #------------------------------------ 27 | 28 | # Segment.com key 29 | ANALYTICS_KEY="" 30 | 31 | # Google Analytics ID 32 | GA_ID="" 33 | 34 | #------------------------------------ 35 | # 4. Cache & settings 36 | #------------------------------------ 37 | 38 | # Default Tab: products cache key 39 | PRODUCTS_CACHE_KEY="ph.chrome.products" 40 | 41 | #------------------------------------ 42 | # 5. Core 43 | #------------------------------------ 44 | 45 | # Google Chrome Extension ID 46 | EXTENSION_ID="likjafohlgffamccflcidmedfongmkee" 47 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "version": "0.1.2", 4 | "manifest_version": 2, 5 | "description": "__MSG_appDescription__", 6 | "chrome_url_overrides": { 7 | "newtab": "apps/tabs/main.html" 8 | }, 9 | "background": { 10 | "scripts": [ 11 | "vendor/analytics.js", 12 | "apps/background/main.js" 13 | ] 14 | }, 15 | "icons": { 16 | "16": "common/icons/icon-16.png", 17 | "128": "common/icons/icon-128.png" 18 | }, 19 | "default_locale": "en", 20 | "browser_action": { 21 | "default_icon": { 22 | "19": "common/icons/icon-19.png", 23 | "38": "common/icons/icon-38.png" 24 | }, 25 | "default_title": "__MSG_title__", 26 | "default_popup": "apps/popup/main.html" 27 | }, 28 | "omnibox": { 29 | "keyword": "ph" 30 | }, 31 | "permissions": [ 32 | "*://www.producthunt.com/*", 33 | "webRequestBlocking", 34 | "storage", 35 | "webRequest", 36 | "tabs" 37 | ], 38 | "web_accessible_resources": [ 39 | "apps/content/product-bar.css", 40 | "apps/content/assets/comment.svg", 41 | "apps/content/assets/arrow_up.svg", 42 | "common/loader/loader.png", 43 | "common/loader/loader@2x.png" 44 | ], 45 | "content_security_policy": "script-src https://ssl.google-analytics.com 'self' 'unsafe-eval'; object-src 'self'" 46 | } 47 | -------------------------------------------------------------------------------- /src/common/settings/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let debug = require('debug')('ph:settings'); 6 | let nullStorage = { 7 | get: function(keys, cb) { 8 | cb(keys); 9 | }, 10 | set: function(items, cb) { 11 | cb(); 12 | } 13 | }; 14 | 15 | /** 16 | * Chrome Extension Settings. 17 | */ 18 | 19 | let settings = { 20 | 21 | /** 22 | * Get `key`. 23 | * 24 | * @param {String} key 25 | * @param {Function} callback 26 | * @public 27 | */ 28 | 29 | get(key, cb) { 30 | this.storage().get({ [key]: false }, function(items) { 31 | debug('%j', items); 32 | cb(items[key]); 33 | }); 34 | }, 35 | 36 | /** 37 | * Get all `keys`. 38 | * 39 | * @param {Object} keys 40 | * @param {Function} callback 41 | * @public 42 | */ 43 | 44 | getAll(keys, cb) { 45 | this.storage().get(keys, cb); 46 | }, 47 | 48 | /** 49 | * Set `items`. 50 | * 51 | * @param {Object} items 52 | * @param {Function} callback 53 | * @public 54 | */ 55 | 56 | setAll(items, cb) { 57 | this.storage().set(items, cb); 58 | }, 59 | 60 | /** 61 | * Return the current storage. 62 | * 63 | * @returns {Object} 64 | * @public 65 | */ 66 | 67 | storage() { 68 | return chrome.storage ? chrome.storage.sync : nullStorage; 69 | } 70 | }; 71 | 72 | /** 73 | * Export `settings`. 74 | */ 75 | 76 | module.exports = settings; 77 | -------------------------------------------------------------------------------- /src/common/analytics/__tests__/Tracker-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | 3 | describe('Tracker', function() { 4 | let Tracker = require('../Tracker'); 5 | let post = { id: '2', name: 'name' }; 6 | let stubStorage = { 7 | get: function(keys, cb) { 8 | cb({ userId: '1' }); 9 | } 10 | }; 11 | 12 | describe('#clickPost', function() { 13 | it('tracks a post click event', function() { 14 | let analytics = { track: jest.genMockFn() }; 15 | let tracker = new Tracker(analytics, stubStorage); 16 | 17 | tracker.clickPost(post); 18 | 19 | expect(analytics.track).toBeCalledWith({ 20 | anonymousId: '1', 21 | event: 'click', 22 | properties: { 23 | type: 'post', 24 | link_location: 'index', 25 | platform: 'chrome extension', 26 | post_id: post.id, 27 | post_name: post.name 28 | } 29 | }); 30 | }); 31 | }); 32 | 33 | describe('#clickBar', function() { 34 | it('tracks a post click event', function() { 35 | let analytics = { track: jest.genMockFn() }; 36 | let tracker = new Tracker(analytics, stubStorage); 37 | 38 | tracker.clickBar(post); 39 | 40 | expect(analytics.track).toBeCalledWith({ 41 | anonymousId: '1', 42 | event: 'click', 43 | properties: { 44 | type: 'post', 45 | link_location: 'top_bar', 46 | platform: 'chrome extension', 47 | post_id: post.id, 48 | post_name: post.name 49 | } 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/common/api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let debug = require('debug')('ph:api'); 6 | let PostActions = require('../actions/PostActions'); 7 | let ProductHunt = require('../product-hunt'); 8 | let cache = require('lscache'); 9 | 10 | /** 11 | * Constants. 12 | */ 13 | 14 | const BASE_URL = process.env.API_BASE_URL; 15 | const OAUTH_KEY = process.env.OAUTH_KEY; 16 | const OAUTH_SECRET = process.env.OAUTH_SECRET; 17 | const CACHE_KEY = process.env.PRODUCTS_CACHE_KEY; 18 | const CACHE_DURARTION = 60 * 24; 19 | 20 | /** 21 | * Locals. 22 | */ 23 | 24 | let ph = new ProductHunt(OAUTH_KEY, OAUTH_SECRET, BASE_URL); 25 | 26 | /** 27 | * API actions. 28 | */ 29 | 30 | let api = { 31 | 32 | /** 33 | * Fetch post by `url`. 34 | * 35 | * @param {String} url 36 | * @public 37 | */ 38 | 39 | getPost(url) { 40 | debug('getting post with url %s', url); 41 | 42 | ph.searchPosts({ 'search[url]': url }, function(err, posts) { 43 | if (err) throw err; 44 | debug('post received'); 45 | PostActions.receivePost(posts[0]); 46 | }); 47 | }, 48 | 49 | /** 50 | * Fetch all posts for given date. 51 | * 52 | * @param {Date} date 53 | * @param {Function} callback [optional] 54 | * @public 55 | */ 56 | 57 | getPosts(daysAgo, cb) { 58 | debug('getting posts from %d days ago', daysAgo); 59 | 60 | ph.getPosts(daysAgo, function(err, posts) { 61 | if (err) throw err; 62 | debug('posts received'); 63 | 64 | if (daysAgo === 0) { 65 | debug('caching the posts...'); 66 | cache.set(CACHE_KEY, posts, CACHE_DURARTION); 67 | } 68 | 69 | PostActions.receivePosts(posts); 70 | cb(); 71 | }); 72 | }, 73 | }; 74 | 75 | /** 76 | * Export `api`. 77 | */ 78 | 79 | module.exports = api; 80 | -------------------------------------------------------------------------------- /src/apps/popup/components/tab.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | let React = require('react'); 6 | let ReactDOM = require('react-dom'); 7 | let debug = require('debug')('ph:popup:tab'); 8 | 9 | /** 10 | * iframe Tab View. 11 | * 12 | * Usage: 13 | * 14 | * ```js 15 | * 16 | * ``` 17 | * 18 | * Properties: 19 | * 20 | * - url: Post URL 21 | * 22 | * @class 23 | */ 24 | 25 | let Tab = React.createClass({ 26 | 27 | /** 28 | * Hide the iframe and show the loader. 29 | */ 30 | 31 | componentDidUpdate() { 32 | ReactDOM.findDOMNode(this).querySelector('iframe').style.setProperty('display', 'none'); 33 | ReactDOM.findDOMNode(this).querySelector('#loader').style.setProperty('display', 'block'); 34 | }, 35 | 36 | /** 37 | * Bind to iframe's load event so we can hide the 38 | * laoder and show the iframe. 39 | */ 40 | 41 | componentDidMount() { 42 | let iframe = ReactDOM.findDOMNode(this).querySelector('iframe'); 43 | let loader = ReactDOM.findDOMNode(this).querySelector('#loader'); 44 | 45 | iframe.style.setProperty('display', 'none'); 46 | loader.style.setProperty('display', 'block'); 47 | 48 | iframe.onload = () => { 49 | debug('tab loaded'); 50 | loader.style.setProperty('display', 'none'); 51 | iframe.style.setProperty('display', 'block'); 52 | }; 53 | }, 54 | 55 | /** 56 | * Render the view. 57 | */ 58 | 59 | render() { 60 | debug('loading tab with URL %s', this.props.url); 61 | 62 | return ( 63 |
64 |
65 |