├── img └── loading.gif ├── .gitignore ├── test ├── stubs │ └── api │ │ ├── Dockerfile │ │ ├── package.json │ │ └── server.js ├── Dockerfile ├── package.json ├── nightwatch.json ├── index.story.test.html ├── index.test.html ├── features.headline.js └── features.story.js ├── js ├── components │ ├── Title.jsx │ ├── Loading.jsx │ ├── EmptyStories.jsx │ ├── Layout.jsx │ ├── ComponentBuilder.jsx │ ├── Counter.jsx │ ├── HeadlineItem.jsx │ ├── StoryItem.jsx │ ├── Story.jsx │ └── Viewer.jsx ├── Downloader.js ├── Storage.js ├── App.js └── Keyboard.js ├── Dockerfile ├── Makefile ├── .eslintrc.json ├── webpack.config.js ├── docker-compose.yml ├── server.js ├── index.html ├── dist ├── index.html └── minimal_viewer-0.3.1.css ├── LICENSE ├── package.json ├── css └── app.css └── README.md /img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiguelBel/MinimalViewer/HEAD/img/loading.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test/screenshots/ 2 | node_modules 3 | npm-debug.log 4 | *.backup 5 | minimal_viewer.js 6 | -------------------------------------------------------------------------------- /test/stubs/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node 2 | COPY package.json /api/package.json 3 | WORKDIR /api 4 | RUN npm install 5 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7.1.0 2 | RUN mkdir -p /functional_test_suite 3 | COPY package.json /functional_test_suite/package.json 4 | WORKDIR /functional_test_suite 5 | RUN npm install 6 | -------------------------------------------------------------------------------- /test/stubs/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stub_api", 3 | "version": "0.1.0", 4 | "description": "Stub API", 5 | "main": "server.js", 6 | "dependencies": { 7 | "express": "^4.14.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /js/components/Title.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Title = ({ text }) => ( 4 |
5 |

{text}

6 |
7 | ) 8 | 9 | export default Title 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node 2 | RUN mkdir -p /minimal_viewer 3 | COPY package.json /minimal_viewer/package.json 4 | COPY webpack.config.js /minimal_viewer/webpack.config.js 5 | WORKDIR /minimal_viewer 6 | RUN npm install 7 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functional_suite", 3 | "version": "0.1.0", 4 | "description": "Functional test suite", 5 | "main": "test.js", 6 | "scripts": { 7 | "test": "nightwatch *.js" 8 | }, 9 | "dependencies": { 10 | "nightwatch": "^0.9.8" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /js/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = ({ color }) => ( 4 |
5 |
6 | Loading... 7 |
8 |
9 | ) 10 | 11 | export default Loading 12 | -------------------------------------------------------------------------------- /js/components/EmptyStories.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const EmptyStories = () => ( 4 |
5 |
6 |

Woops! There is nothing else for you, yet...

7 |
8 |
9 | ) 10 | 11 | export default EmptyStories 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | create_version: 2 | docker-compose exec minimal_viewer npm run build 3 | rm dist/minimal_viewer-*.js | true 4 | rm dist/minimal_viewer-*.css | true 5 | mv dist/minimal_viewer.js dist/minimal_viewer-$$(npm run get-version --silent).js 6 | mv dist/minimal_viewer.css dist/minimal_viewer-$$(npm run get-version --silent).css -------------------------------------------------------------------------------- /js/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Title from './Title' 3 | 4 | const Layout = ({ children, color, id, title }) => ( 5 |
6 |
7 | 8 | {children} 9 | </div> 10 | ) 11 | 12 | export default Layout 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard", 4 | "standard-react", 5 | "plugin:react/recommended" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "mocha": true, 10 | "node": true 11 | }, 12 | "plugins": [ 13 | "standard", 14 | "react" 15 | ], 16 | "rules": { 17 | "react/sort-comp": "error", 18 | "react/sort-prop-types": "error" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/nightwatch.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_folders": ["."], 3 | "output_folder": false, 4 | "test_settings" : { 5 | "default" : { 6 | "launch_url": "http://minimal_viewer", 7 | "selenium_port": 4444, 8 | "selenium_host": "chromedriver", 9 | "desiredCapabilities": { 10 | "browserName": "chrome", 11 | "javascriptEnabled": true 12 | }, 13 | "screenshots": { 14 | "enabled": true, 15 | "path": "screenshots/" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /js/Downloader.js: -------------------------------------------------------------------------------- 1 | import jmespath from 'jmespath' 2 | 3 | const Downloader = { 4 | create (url, fn, root) { 5 | this.API_URL = url 6 | this.download_stories(fn, root) 7 | }, 8 | 9 | download_stories (fn, root) { 10 | fetch(this.API_URL).then(response => { 11 | response.json().then(parsed => { 12 | if (root) { 13 | fn(jmespath.search(parsed, root)) 14 | } else { 15 | fn(parsed) 16 | } 17 | }) 18 | }) 19 | } 20 | } 21 | 22 | export default Downloader 23 | -------------------------------------------------------------------------------- /js/components/ComponentBuilder.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import EmptyStories from './EmptyStories' 3 | import Loading from './Loading' 4 | import Story from './Story' 5 | 6 | const ComponentBuilder = ({ color, isEmpty, isLoading, ...story }) => { 7 | 8 | if (isEmpty) return ( 9 | <EmptyStories /> 10 | ) 11 | 12 | if (isLoading) return ( 13 | <Loading color={color} /> 14 | ) 15 | 16 | return ( 17 | <Story 18 | {...story} 19 | color={color} 20 | /> 21 | ) 22 | } 23 | 24 | export default ComponentBuilder 25 | -------------------------------------------------------------------------------- /js/components/Counter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Counter = ({ color, current, total, progress }) => ( 4 | <div> 5 | <div className={'top-progress-bar'} style={{backgroundColor: color, width: `${progress}%`}}></div> 6 | <div className='counter'> 7 | <div className='indicator'> 8 | <p id='stories-counter'> 9 | <span className='current'style={{ color }}>{ current }</span> 10 | / 11 | { total } 12 | </p> 13 | </div> 14 | </div> 15 | </div> 16 | ) 17 | 18 | export default Counter 19 | -------------------------------------------------------------------------------- /js/Storage.js: -------------------------------------------------------------------------------- 1 | const EMPTY = [] 2 | 3 | const Storage = { 4 | retrieve (identifier) { 5 | const items = JSON.parse( 6 | localStorage.getItem(`viewer_${identifier}`) 7 | ) 8 | 9 | return items || EMPTY 10 | }, 11 | 12 | store (identifier, element) { 13 | const store = this.retrieve(identifier) 14 | const isAlreadyRegistered = store.includes(element) 15 | 16 | if (isAlreadyRegistered) return 17 | 18 | store.push(element) 19 | localStorage.setItem( 20 | `viewer_${identifier}`, 21 | JSON.stringify(store) 22 | ) 23 | } 24 | } 25 | 26 | export default Storage 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports ={ 4 | entry: ['whatwg-fetch', './js/App.js'], 5 | output: { 6 | filename: 'minimal_viewer.js', 7 | path: '/minimal_viewer/dist/', 8 | library: 'MinimalViewer' 9 | }, 10 | module: { 11 | loaders: [ 12 | { 13 | test: /\.jsx?$/, 14 | loader: 'babel-loader', 15 | exclude: /node_modules/, 16 | query: { 17 | presets: ['es2015', 'stage-0', 'react'] 18 | } 19 | }, 20 | ] 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.jsx', '.json'], 24 | root: [ 25 | path.resolve('./js') 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/stubs/api/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var path = require('path'); 4 | 5 | app.use(express.static(process.cwd())); 6 | 7 | app.get('/', function(req, res) { 8 | res.setHeader('Content-Type', 'application/json'); 9 | res.setHeader('Access-Control-Allow-Origin', '*'); 10 | var fixture = { 'wadus': { 'another_key': []} }; 11 | 12 | for (var i = 1; i <= 30; i++) { 13 | fixture['wadus']['another_key'].push({ 14 | "data": { title: 'Title ' + i, url: 'http://www.' + i + '.com', id: String(i), domain: 'www.' + i + '.com' } 15 | }); 16 | } 17 | 18 | res.send(JSON.stringify(fixture)); 19 | }); 20 | 21 | app.listen(process.env.PORT, function () { 22 | console.log('[APP] Listening in port ' + process.env.PORT); 23 | }) -------------------------------------------------------------------------------- /js/components/HeadlineItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HeadLineItem = ({ color, link, subtitle, title }) => { 4 | const handleMouseOver = (e) => { 5 | const { target: { style } } = e 6 | 7 | style.color = color 8 | } 9 | 10 | const handleMouseOut = (e) => { 11 | const { target: { style } } = e 12 | 13 | style.color = 'inherit' 14 | } 15 | 16 | return ( 17 | <div id='story-link' className='container Headline'> 18 | <div 19 | id='url-container' 20 | className='Title' 21 | onMouseOver={handleMouseOver} 22 | onMouseOut={handleMouseOut} 23 | > 24 | <a href={link} id='story-url' target='_blank'>{title}</a> 25 | </div> 26 | <p className='Subtitle'>{subtitle}</p> 27 | </div> 28 | ) 29 | } 30 | 31 | export default HeadLineItem 32 | -------------------------------------------------------------------------------- /js/components/StoryItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const StoryItem = ({ color, link, subtitle, title }) => { 4 | const handleMouseOver = (e) => { 5 | const { target: { style } } = e 6 | 7 | style.color = color 8 | } 9 | 10 | const handleMouseOut = (e) => { 11 | const { target: { style } } = e 12 | 13 | style.color = 'inherit' 14 | } 15 | 16 | return ( 17 | <div id='story-link' className='container Story'> 18 | <div 19 | id='url-container' 20 | className='Title' 21 | onMouseOver={handleMouseOver} 22 | onMouseOut={handleMouseOut} 23 | > 24 | <a href={link} id='story-url' target='_blank'>{title}</a> 25 | </div> 26 | <div className='Separator'></div> 27 | <p className='Subtitle'>{subtitle}</p> 28 | </div> 29 | ) 30 | } 31 | 32 | export default StoryItem 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | chromedriver: 2 | image: selenium/standalone-chrome 3 | links: 4 | - minimal_viewer 5 | - api_stub 6 | ports: 7 | - "4444:4444" 8 | 9 | minimal_viewer: 10 | build: . 11 | environment: 12 | - PORT=9300 13 | - URL=http://localhost:9400 14 | ports: 15 | - 9300:9300 16 | links: 17 | - api_stub 18 | volumes: 19 | - '.:/minimal_viewer' 20 | - '/minimal_viewer/node_modules' 21 | command: node server.js 22 | 23 | api_stub: 24 | build: ./test/stubs/api 25 | environment: 26 | - PORT=9400 27 | ports: 28 | - 9400:9400 29 | volumes: 30 | - './test/stubs/api:/api' 31 | - '/api/node_modules' 32 | command: node server.js 33 | 34 | functional_test_suite: 35 | build: ./test 36 | links: 37 | - chromedriver 38 | volumes: 39 | - './test/:/functional_test_suite' 40 | - '/functional_test_suite/node_modules' 41 | command: sleep infinity -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var webpack = require('webpack'); 3 | var path = require('path'); 4 | var config = require('./webpack.config'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | stats: { 11 | colors: true 12 | } 13 | })); 14 | 15 | app.use(require('webpack-hot-middleware')(compiler)); 16 | app.use(express.static(process.cwd())); 17 | 18 | app.get('/', function(req, res) { 19 | res.sendFile(path.join(__dirname + '/index.html')); 20 | }); 21 | 22 | app.get('/test', function(req, res) { 23 | res.sendFile(path.join(__dirname + '/test/index.test.html')); 24 | }); 25 | 26 | app.get('/test_story', function(req, res) { 27 | res.sendFile(path.join(__dirname + '/test/index.story.test.html')); 28 | }); 29 | 30 | app.listen(process.env.PORT, function () { 31 | console.log('[APP] Listening in port ' + process.env.PORT); 32 | }) 33 | -------------------------------------------------------------------------------- /test/index.story.test.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | 4 | <title>Minimal Viewer Test 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/index.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Minimal Viewer Test 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Minimal Viewer 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Minimal Viewer 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /js/components/Story.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import jmespath from 'jmespath' 3 | 4 | import HeadlineItem from './HeadlineItem' 5 | import StoryItem from './StoryItem' 6 | import Counter from './Counter' 7 | 8 | const TEMPLATES = { 9 | 'headline': HeadlineItem, 10 | 'story': StoryItem 11 | } 12 | const DEFAULT_TEMPLATE = HeadlineItem 13 | 14 | const Story = ({ color, queueIndex, queueSize, relations, story, type }) => { 15 | const Template = TEMPLATES[type] || DEFAULT_TEMPLATE 16 | let progress = (queueIndex / queueSize) * 100 17 | 18 | return ( 19 |
20 |