diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..d4eed84 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = false +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..a29125a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,106 @@ +# From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes + +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto + +# +# The above will handle all files NOT found below +# + +# +## These files are text and should be normalized (Convert crlf => lf) +# + +# source code +*.php text +*.css text +*.sass text +*.scss text +*.less text +*.styl text +*.js text eol=lf +*.coffee text +*.json text +*.htm text +*.html text +*.xml text +*.svg text +*.txt text +*.ini text +*.inc text +*.pl text +*.rb text +*.py text +*.scm text +*.sql text +*.sh text +*.bat text + +# templates +*.ejs text +*.hbt text +*.jade text +*.haml text +*.hbs text +*.dot text +*.tmpl text +*.phtml text + +# server config +.htaccess text + +# git config +.gitattributes text +.gitignore text +.gitconfig text + +# code analysis config +.jshintrc text +.jscsrc text +.jshintignore text +.csslintrc text + +# misc config +*.yaml text +*.yml text +.editorconfig text + +# build config +*.npmignore text +*.bowerrc text + +# Heroku +Procfile text +.slugignore text + +# Documentation +*.md text +LICENSE text +AUTHORS text + + +# +## These files are binary and should be left untouched +# + +# (binary is a macro for -text -diff) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mov binary +*.mp4 binary +*.mp3 binary +*.flv binary +*.fla binary +*.swf binary +*.gz binary +*.zip binary +*.7z binary +*.ttf binary +*.eot binary +*.woff binary +*.pyc binary +*.pdf binary diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..0012879 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Don't check auto-generated stuff into git +coverage +build +node_modules +stats.json +releases + +# Cruft +.DS_Store +npm-debug.log +.idea diff --git a/README.md b/README.md index 025b8dc..f1eafd7 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,43 @@ -# live.hyperfox.org +# [live.hyperfox.org](https://github.com/xiam/hyperfox-live) +> This is the source code of the [Hyperfox][1]'s [live visualization tool][2]. -This is the source code of the [Hyperfox][1]'s [live visualization tool][2]. ## Requisites -* ruby -* rubygems -* nodejs (I know, I know...) -* npm +* Node.js (>=6) +* npm (>=3) ## Initial setup ``` -gem install compass - -cd html npm install - -sudo npm install -g bower -sudo npm install -g grunt-cli - -bower update -npm install grunt-contrib-compass --save-dev - ``` ## Development +```bash +# start a webpack server for development +# at http://localhost:3000 +npm run start:development ``` -cd html -grunt serve + +## Build +See [electron-packager](https://github.com/electron-userland/electron-packager) for electron package requirements. + +```bash +# create the webpack build in the `build` folder +npm run build +# create an electron package for all the platforms +npm run package +# create an electron package for osx +npm run package:osx +# create an electron package for windows +npm run package:win +# create an electron package for linux +npm run package:linux ``` + # License (MIT) > Copyright (c) 2014-2015 José Carlos Nieto, https://menteslibres.net/xiam diff --git a/app/app.js b/app/app.js new file mode 100755 index 0000000..ed45b44 --- /dev/null +++ b/app/app.js @@ -0,0 +1,101 @@ +/** +app.js +This is the entry file for the application, only setup and boilerplate code. +**/ + +import 'babel-polyfill' + +// Load the manifest.json file and the .htaccess file +import 'file?name=[name].[ext]!./manifest.json' + +// Import all the third party stuff +import React from 'react' +import ReactDOM from 'react-dom' +import { Provider } from 'react-redux' +import { applyRouterMiddleware, Router, browserHistory, hashHistory } from 'react-router' +import { syncHistoryWithStore } from 'react-router-redux' +import FontFaceObserver from 'fontfaceobserver'; +import { useScroll } from 'react-router-scroll'; +import configureStore from './store' + +// Import Language Provider +import LanguageProvider from 'containers/LanguageProvider' +// Import i18n messages +import { translationMessages } from './i18n' + +// Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder +// import 'sanitize.css/lib/sanitize.css' +import styles from 'containers/App/styles.css'; +import 'bulma/css/bulma.css' + +const openSansObserver = new FontFaceObserver('Open Sans', {}); + +// When Open Sans is loaded, add a font-family using Open Sans to the body +openSansObserver.load().then(() => { + document.body.classList.add(styles.fontLoaded); +}, () => { + document.body.classList.remove(styles.fontLoaded); +}) + +// Create redux store with history +// this uses the singleton browserHistory provided by react-router +// Optionally, this could be changed to leverage a created history +// e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` +const initialState = {} +const store = configureStore(initialState, browserHistory) + +// Sync history and store, as the react-router-redux reducer +// is under the non-default key ("routing"), selectLocationState +// must be provided for resolving how to retrieve the "route" in the state +import { selectLocationState } from 'containers/App/selectors' +const history = syncHistoryWithStore(hashHistory, store, { + selectLocationState: selectLocationState() +}) + +// Set up the router, wrapping all Routes in the App component +import App from 'containers/App' +import createRoutes from './routes' +const rootRoute = { + component: App, + childRoutes: createRoutes(store) +} +const render = () => { + ReactDOM.render( + + + + + , + document.getElementById('app') + ) +} +// Hot reloadable translation json files +if (module.hot) { + module.hot.accept('./i18n', () => { + render(translationMessages) + }) +} + +// Chunked polyfill for browsers without Intl support +if (!window.Intl) { + (new Promise((resolve) => { + resolve(System.import('intl')); + })) + .then(() => Promise.all([ + System.import('intl/locale-data/jsonp/en.js'), + System.import('intl/locale-data/jsonp/de.js'), + ])) + .then(() => render(translationMessages)) + .catch((err) => { + throw err; + }); +} else { + render(translationMessages); +} +// Install ServiceWorker and AppCache in the end since +// it's not most important operation and if main code fails, +// we do not want it installed +// import { install } from 'offline-plugin/runtime' +// install() diff --git a/app/components/Icons/Logo.js b/app/components/Icons/Logo.js new file mode 100644 index 0000000..92dcf1f --- /dev/null +++ b/app/components/Icons/Logo.js @@ -0,0 +1,22 @@ +import React, {PropTypes} from 'react' + +export default function Logo (props) { + return ( +
+ + + +
+ ) +} + +Logo.propTypes = { + className: PropTypes.string, + width: PropTypes.string, + height: PropTypes.string +} diff --git a/app/components/NavigationBar/index.js b/app/components/NavigationBar/index.js new file mode 100755 index 0000000..de365e0 --- /dev/null +++ b/app/components/NavigationBar/index.js @@ -0,0 +1,35 @@ +import React from 'react' + +import classNames from 'classnames' +import styles from './styles.css' + +import {FormattedMessage} from 'react-intl' +import messages from './messages' + +import Logo from 'components/Icons/Logo' + +export default function NavigationBar () { + return ( +
+ +
+ ) +} diff --git a/app/components/NavigationBar/messages.js b/app/components/NavigationBar/messages.js new file mode 100644 index 0000000..7833668 --- /dev/null +++ b/app/components/NavigationBar/messages.js @@ -0,0 +1,8 @@ +import { defineMessages } from 'react-intl' + +export default defineMessages({ + searchMessage: { + id: 'hyperfox.components.NavigationBar.search.message', + defaultMessage: 'Search.' + } +}) diff --git a/app/components/NavigationBar/styles.css b/app/components/NavigationBar/styles.css new file mode 100755 index 0000000..7ec25d4 --- /dev/null +++ b/app/components/NavigationBar/styles.css @@ -0,0 +1,30 @@ +.NavigationBar{ + background: #455a64 !important; + padding: 0 10px; + &__Logo{ + &__Live{ + font-size: 20px; + color: #fff; + padding: 0; + } + } + &__Search{ + &__Input{ + .fa{ + font-size: 15px !important; + } + } + } +} + +.Logo{ + path{ + fill: #ed6c63; + transition: fill .4s linear; + } + &:hover{ + path{ + fill: #DB0F2E; + } + } +} diff --git a/app/components/NavigationBar/tests/index.test.js b/app/components/NavigationBar/tests/index.test.js new file mode 100755 index 0000000..629b0a6 --- /dev/null +++ b/app/components/NavigationBar/tests/index.test.js @@ -0,0 +1,20 @@ +/* Testing our NavigationBar component */ + +import NavigationBar from '../index' + +import Logo from 'components/Icons/Logo' +import styles from '../styles.css' + +import expect from 'expect' +import { shallow } from 'enzyme' +import React from 'react' + +describe('', () => { + it('should render the Logo', () => { + const navLogo = () + const renderedComponent = shallow( + + ) + expect(renderedComponent.contains(navLogo)).toEqual(true) + }) +}) diff --git a/app/components/Notification/index.js b/app/components/Notification/index.js new file mode 100644 index 0000000..426ab9f --- /dev/null +++ b/app/components/Notification/index.js @@ -0,0 +1,30 @@ +import React, {PropTypes} from 'react' +import {FormattedMessage} from 'react-intl' +import classNames from 'classnames' + +export default class Notification extends React.Component { + static propTypes = { + alertStyle: PropTypes.string, + message: PropTypes.object + } + + constructor (props) { + super(props) + } + + render () { + const alertStyles = { + normal: '', + info: 'is-info', + success: 'is-success', + warning: 'is-warning', + danger: 'is-danger' + } + return ( +
+ + +
+ ) + } +} diff --git a/app/components/Notification/tests/index.test.js b/app/components/Notification/tests/index.test.js new file mode 100644 index 0000000..ea3e487 --- /dev/null +++ b/app/components/Notification/tests/index.test.js @@ -0,0 +1,31 @@ +/* Testing our NavigationBar component */ + +import Notification from '../index' + +import expect from 'expect' +import { shallow } from 'enzyme' +import React from 'react' + +const message = { + id: 'hyperfox.test', + description: 'Major Tom', + defaultMessage: 'Ground Control To Major Tom' +} + +describe('', () => { + it('should render the Notification with is-danger class', () => { + const renderedComponent = shallow( + + ) + expect(renderedComponent.hasClass('is-danger')).toEqual(true) + }) + it('should render the Notification with is-success class', () => { + const renderedComponent = shallow( + + ) + expect(renderedComponent.hasClass('is-success')).toEqual(true) + }) +}) diff --git a/app/components/Pagination/index.js b/app/components/Pagination/index.js new file mode 100644 index 0000000..5b2ce67 --- /dev/null +++ b/app/components/Pagination/index.js @@ -0,0 +1,39 @@ +import React from 'react' + +import styles from './styles.css' + +export default function Pagination () { + return ( + + ) +} diff --git a/app/components/Pagination/messages.js b/app/components/Pagination/messages.js new file mode 100644 index 0000000..7833668 --- /dev/null +++ b/app/components/Pagination/messages.js @@ -0,0 +1,8 @@ +import { defineMessages } from 'react-intl' + +export default defineMessages({ + searchMessage: { + id: 'hyperfox.components.NavigationBar.search.message', + defaultMessage: 'Search.' + } +}) diff --git a/app/components/Pagination/styles.css b/app/components/Pagination/styles.css new file mode 100644 index 0000000..a7645ac --- /dev/null +++ b/app/components/Pagination/styles.css @@ -0,0 +1,4 @@ +.Pagination{ + background-color: #455a64; + padding: 10px; +} diff --git a/app/components/Pagination/tests/index.test.js b/app/components/Pagination/tests/index.test.js new file mode 100644 index 0000000..4223173 --- /dev/null +++ b/app/components/Pagination/tests/index.test.js @@ -0,0 +1,11 @@ +/* Testing our Pagination component */ + +import Pagination from '../index' + +import expect from 'expect' +import { shallow } from 'enzyme' +import React from 'react' + +describe('', () => { + +}) diff --git a/app/components/Table/TableBody.js b/app/components/Table/TableBody.js new file mode 100644 index 0000000..7ff209b --- /dev/null +++ b/app/components/Table/TableBody.js @@ -0,0 +1,23 @@ +/* TableBody */ + +import React, {PropTypes} from 'react' +import TableRow from './TableRow' + +export default function TableBody (props) { + const columns = props.columns + const data = props.data + + return ( + + {data.map((item, idx) => { + return + })} + + ) +} + +TableBody.propTypes = { + columns: PropTypes.array, + data: PropTypes.array, + className: PropTypes.string +} diff --git a/app/components/Table/TableHeader.js b/app/components/Table/TableHeader.js new file mode 100644 index 0000000..bf733dd --- /dev/null +++ b/app/components/Table/TableHeader.js @@ -0,0 +1,26 @@ +/* TableHeader */ + +import React, {PropTypes} from 'react' + +export default function TableHeader (props) { + const columns = props.columns + const sort = (column) => { + return (event) => { + const sortDir = props.sortDir + props.onSort(column, sortDir) + } + } + + return ( + + {columns.map((c, i) => { + return {c} + })} + + ) +} + +TableHeader.propTypes = { + columns: PropTypes.array, + sortDir: PropTypes.func +} diff --git a/app/components/Table/TableRow.js b/app/components/Table/TableRow.js new file mode 100644 index 0000000..9108c52 --- /dev/null +++ b/app/components/Table/TableRow.js @@ -0,0 +1,25 @@ +/* TableRow */ + +import React, {PropTypes} from 'react' + +export default function TableRow (props) { + const columns = props.columns + const data = props.data + + const td = (item) => { + return columns.map((c, i) => { + return {item[c]} + }) + } + + return ( + + {td(data)} + + ) +} + +TableRow.propTypes = { + columns: PropTypes.array, + data: PropTypes.object +} diff --git a/app/components/Table/index.js b/app/components/Table/index.js new file mode 100644 index 0000000..4e85647 --- /dev/null +++ b/app/components/Table/index.js @@ -0,0 +1,68 @@ +/* Table */ + +import React, {PropTypes} from 'react' +import TableHeader from './TableHeader' +import TableBody from './TableBody' + +import styles from './styles.css' +import classNames from 'classnames' + +export default class Table extends React.Component { + static propTypes = { + data: PropTypes.array + } + + constructor (props) { + super(props) + this.state = { + data: null, + sortDir: {} + } + this.sortByColumn = this.sortByColumn.bind(this) + this.sort = this.sort.bind(this) + } + + componentWillMount () { + this.setState({data: this.props.data}) + } + + getColumnNames () { + var firstEl = this.state.data[0] + return Object.keys(firstEl) + } + + sortByColumn (array, column, sortDir) { + return array.sort(function (a, b) { + var x = a[column] + var y = b[column] + if (sortDir === 'asc') { + return ((x > y) ? -1 : ((x < y) ? 1 : 0)) + } else { + return ((x < y) ? -1 : ((x > y) ? 1 : 0)) + } + }) + } + + sort (column) { + var sortDir = this.state.sortDir + var data = this.state.data + var sortedData = this.sortByColumn(data, column, sortDir[column]) + this.setState({data: sortedData}) + sortDir[column] = (sortDir[column] === 'asc' ? 'dsc' : 'asc') + this.setState({sortDir: sortDir}) + } + + render () { + var columns = this.getColumnNames() + var data = this.state.data + + return ( + + + + + +
+ ) + } +} diff --git a/app/components/Table/styles.css b/app/components/Table/styles.css new file mode 100644 index 0000000..9177254 --- /dev/null +++ b/app/components/Table/styles.css @@ -0,0 +1,9 @@ +.Table{ + table-layout: fixed; + &__TableHead{ + background-color: #666; + } + &__TableBody{ + overflow: scroll; + } +} diff --git a/app/containers/App/index.js b/app/containers/App/index.js new file mode 100755 index 0000000..49b069b --- /dev/null +++ b/app/containers/App/index.js @@ -0,0 +1,27 @@ +/* App.react.js + * This component is the skeleton around the actual pages, and should only + * contain code that should be seen on all pages. (e.g. navigation bar) + */ + +import React, {PropTypes} from 'react' +import NavigationBar from 'components/NavigationBar' +import Pagination from 'components/Pagination' + +import classNames from 'classnames' +import styles from './styles.css' + +export default class App extends React.Component { + static propTypes = { + children: PropTypes.node + } + + render () { + return ( +
+ + {this.props.children} + +
+ ) + } +} diff --git a/app/containers/App/selectors.js b/app/containers/App/selectors.js new file mode 100755 index 0000000..e2f8108 --- /dev/null +++ b/app/containers/App/selectors.js @@ -0,0 +1,18 @@ +// selectLocationState expects a plain JS object for the routing state +const selectLocationState = () => { + let prevRoutingState + let prevRoutingStateJS + + return (state) => { + const routingState = state.get('route') // or state.route + + if (!routingState.equals(prevRoutingState)) { + prevRoutingState = routingState + prevRoutingStateJS = routingState.toJS() + } + + return prevRoutingStateJS + } +} + +export {selectLocationState} diff --git a/app/containers/App/styles.css b/app/containers/App/styles.css new file mode 100644 index 0000000..f622865 --- /dev/null +++ b/app/containers/App/styles.css @@ -0,0 +1,5 @@ +.App{ + height: 100vh; + display: flex; + flex-direction: column; +} diff --git a/app/containers/App/tests/selectors.test.js b/app/containers/App/tests/selectors.test.js new file mode 100755 index 0000000..8f43037 --- /dev/null +++ b/app/containers/App/tests/selectors.test.js @@ -0,0 +1,14 @@ +import { fromJS } from 'immutable' +import expect from 'expect' + +import { selectLocationState } from 'containers/App/selectors' + +describe('selectLocationState', () => { + it('should select the route as a plain JS object', () => { + const route = fromJS({ + locationBeforeTransitions: null + }) + const mockedState = fromJS({route}) + expect(selectLocationState()(mockedState)).toEqual(route.toJS()) + }) +}) diff --git a/app/containers/LanguageProvider/actions.js b/app/containers/LanguageProvider/actions.js new file mode 100755 index 0000000..5030d68 --- /dev/null +++ b/app/containers/LanguageProvider/actions.js @@ -0,0 +1,10 @@ +/* LanguageProvider actions */ + +import {CHANGE_LOCALE} from './constants' + +export function changeLocale (languageLocale) { + return { + type: CHANGE_LOCALE, + locale: languageLocale + } +} diff --git a/app/containers/LanguageProvider/constants.js b/app/containers/LanguageProvider/constants.js new file mode 100755 index 0000000..730444c --- /dev/null +++ b/app/containers/LanguageProvider/constants.js @@ -0,0 +1,3 @@ +/* LanguageProvider constants */ + +export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE' diff --git a/app/containers/LanguageProvider/index.js b/app/containers/LanguageProvider/index.js new file mode 100755 index 0000000..ca17662 --- /dev/null +++ b/app/containers/LanguageProvider/index.js @@ -0,0 +1,37 @@ +/* LanguageProvider + * this component connects the redux state language locale to the + * IntlProvider component and i18n messages (loaded from `app/translations`) + */ + +import React from 'react' +import { connect } from 'react-redux' +import { createSelector } from 'reselect' +import { IntlProvider } from 'react-intl' +import { selectLocale } from './selectors' + +export class LanguageProvider extends React.Component { + render () { + return ( + + {this.props.children} + + ) + } +} + +LanguageProvider.propTypes = { + locale: React.PropTypes.string, + messages: React.PropTypes.object, + children: React.PropTypes.element.isRequired +} + +const mapStateToProps = createSelector( + selectLocale(), + (locale) => ({locale}) +) + +function mapDispatchToProps (dispatch) { + return {dispatch} +} + +export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider) diff --git a/app/containers/LanguageProvider/reducer.js b/app/containers/LanguageProvider/reducer.js new file mode 100755 index 0000000..d752735 --- /dev/null +++ b/app/containers/LanguageProvider/reducer.js @@ -0,0 +1,19 @@ +/* LanguageProvider reducer */ + +import { fromJS } from 'immutable' +import {CHANGE_LOCALE} from './constants' + +const initialState = fromJS({ + locale: 'en' +}) + +function languageProviderReducer (state = initialState, action) { + switch (action.type) { + case CHANGE_LOCALE: + return state.set('locale', action.locale) + default: + return state + } +} + +export default languageProviderReducer diff --git a/app/containers/LanguageProvider/selectors.js b/app/containers/LanguageProvider/selectors.js new file mode 100755 index 0000000..cf69135 --- /dev/null +++ b/app/containers/LanguageProvider/selectors.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect' + +/* Direct selector to the languageToggle state domain */ +const selectLanguage = () => state => state.get('language') + +/* Select the language locale */ +const selectLocale = () => createSelector( + selectLanguage(), + (languageState) => languageState.get('locale') +) + +export {selectLanguage, selectLocale} diff --git a/app/containers/LanguageProvider/tests/actions.test.js b/app/containers/LanguageProvider/tests/actions.test.js new file mode 100755 index 0000000..86647e6 --- /dev/null +++ b/app/containers/LanguageProvider/tests/actions.test.js @@ -0,0 +1,15 @@ +import expect from 'expect' +import {changeLocale} from '../actions' +import {CHANGE_LOCALE} from '../constants' + +describe('LanguageProvider actions', () => { + describe('Change Local Action', () => { + it('has a type of CHANGE_LOCALE', () => { + const expected = { + type: CHANGE_LOCALE, + locale: 'es' + } + expect(changeLocale('es')).toEqual(expected) + }) + }) +}) diff --git a/app/containers/LanguageProvider/tests/index.test.js b/app/containers/LanguageProvider/tests/index.test.js new file mode 100755 index 0000000..60191f3 --- /dev/null +++ b/app/containers/LanguageProvider/tests/index.test.js @@ -0,0 +1,35 @@ +import LanguageProvider from '../index' + +import expect from 'expect' +import { shallow } from 'enzyme' +import { FormattedMessage, defineMessages } from 'react-intl' +import configureStore from '../../../store' +import React from 'react' +import { Provider } from 'react-redux' +import { browserHistory } from 'react-router' +import { translatedMessages } from '../../../i18n' + +describe('', () => { + let store + + before(() => { + store = configureStore({}, browserHistory) + }) + + it('should render the default language messages', () => { + const messages = defineMessages({ + someMessage: { + id: 'some.id', + defaultMessage: 'This is some default message' + } + }) + const renderedComponent = shallow( + + + + + + ) + expect(renderedComponent.contains()).toEqual(true) + }) +}) diff --git a/app/containers/LanguageProvider/tests/reducer.test.js b/app/containers/LanguageProvider/tests/reducer.test.js new file mode 100755 index 0000000..f6e37a2 --- /dev/null +++ b/app/containers/LanguageProvider/tests/reducer.test.js @@ -0,0 +1,11 @@ +import expect from 'expect' +import languageProviderReducer from '../reducer' +import { fromJS } from 'immutable' + +describe('languageProviderReducer', () => { + it('returns the initial state', () => { + expect(languageProviderReducer(undefined, {})).toEqual(fromJS({ + locale: 'en' + })) + }) +}) diff --git a/app/containers/LanguageProvider/tests/selectors.test.js b/app/containers/LanguageProvider/tests/selectors.test.js new file mode 100755 index 0000000..ddae78a --- /dev/null +++ b/app/containers/LanguageProvider/tests/selectors.test.js @@ -0,0 +1,14 @@ +import {selectLanguage} from '../selectors' +import {fromJS} from 'immutable' +import expect from 'expect' + +describe('selectLanguage', () => { + const globalSelector = selectLanguage() + it('should select the global state', () => { + const globalState = fromJS({}) + const mockedState = fromJS({ + language: globalState + }) + expect(globalSelector(mockedState)).toEqual(globalState) + }) +}) diff --git a/app/containers/Logger/index.js b/app/containers/Logger/index.js new file mode 100755 index 0000000..6ec8946 --- /dev/null +++ b/app/containers/Logger/index.js @@ -0,0 +1,49 @@ +/* Logger */ + +import React from 'react' +import Table from 'components/Table' + +import styles from './styles.css' + +const logs = [ + { + date: '12:51:12', + method: 'GET', + origin: '11.1.1.143', + destination: 'disney.com', + path: '/', + status: 311, + size: '35.24Kb', + type: 'application/text', + time: '256.71' + }, { + date: '12:51:12', + method: 'GET', + origin: '11.1.1.13', + destination: 'rog.mx', + path: '/', + status: 311, + size: '35.24Kb', + type: 'application/text', + time: '33.71' + }, { + date: '12:51:12', + method: 'GET', + origin: '11.1.1.143', + destination: 'test.mx', + path: '/', + status: 311, + size: '42.24Kb', + type: 'application/text', + time: '133.37' + } +] + +export default function Logger () { + const data = Array.apply(null, Array(42)).map(() => { return logs[Math.floor(Math.random() * 3)] }) + return ( +
+ + + ) +} diff --git a/app/containers/Logger/messages.js b/app/containers/Logger/messages.js new file mode 100644 index 0000000..5b1aa49 --- /dev/null +++ b/app/containers/Logger/messages.js @@ -0,0 +1,12 @@ +import { defineMessages } from 'react-intl' + +export default defineMessages({ + noConnected: { + id: 'hyperfox.containers.Logger.Notification.noconnected.message', + defaultMessage: 'Could not connect to the Hyperfox API. Seems like hyperfox is not running on your local box.' + }, + noData: { + id: 'hyperfox.containers.Logger.Notification.nodata.message', + defaultMessage: 'No data to show yet.' + } +}) diff --git a/app/containers/Logger/styles.css b/app/containers/Logger/styles.css new file mode 100644 index 0000000..99e08bb --- /dev/null +++ b/app/containers/Logger/styles.css @@ -0,0 +1,4 @@ +.Logger{ + flex: 1; + overflow: auto; +} diff --git a/app/containers/NotFoundPage/index.js b/app/containers/NotFoundPage/index.js new file mode 100755 index 0000000..84ca606 --- /dev/null +++ b/app/containers/NotFoundPage/index.js @@ -0,0 +1,14 @@ +/* NotFoundPage + * This is the page we show when the user visits a url that doesn't have a route + */ + +import React from 'react' + +export default class NotFound extends React.Component { + + render () { + return ( +

Page Not Found

+ ) + } +} diff --git a/app/i18n.js b/app/i18n.js new file mode 100644 index 0000000..c848bac --- /dev/null +++ b/app/i18n.js @@ -0,0 +1,30 @@ +/* This will setup the i18n language files and locale data for hiperfox */ +import { addLocaleData } from 'react-intl' + +import enLocaleData from 'react-intl/locale-data/en' +import esLocaleData from 'react-intl/locale-data/es' + +export const appLocales = [ + 'en', + 'es' +] + +import enTranslationMessages from './translations/en.json' +import esTranslationMessages from './translations/es.json' + +addLocaleData(enLocaleData) +addLocaleData(esLocaleData) + +const formatTranslationMessages = (messages) => { + const formattedMessages = {} + for (const message of messages) { + formattedMessages[message.id] = message.message || message.defaultMessage + } + + return formattedMessages +} + +export const translationMessages = { + en: formatTranslationMessages(enTranslationMessages), + es: formatTranslationMessages(esTranslationMessages) +} diff --git a/app/index.html b/app/index.html new file mode 100755 index 0000000..7e4192e --- /dev/null +++ b/app/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + Hyperfox + + +
+ + diff --git a/app/manifest.json b/app/manifest.json new file mode 100755 index 0000000..a2ad609 --- /dev/null +++ b/app/manifest.json @@ -0,0 +1,33 @@ +{ + "name": "Hyperfox", + "icons": [ + { + "src": "favicon.png", + "sizes": "48x48", + "type": "image/png", + "density": 1.0 + }, + { + "src": "favicon.png", + "sizes": "96x96", + "type": "image/png", + "density": 2.0 + }, + { + "src": "favicon.png", + "sizes": "144x144", + "type": "image/png", + "density": 3.0 + }, + { + "src": "favicon.png", + "sizes": "192x192", + "type": "image/png", + "density": 4.0 + } + ], + "start_url": "index.html", + "display": "standalone", + "orientation": "portrait", + "background_color": "#FFFFFF" +} diff --git a/app/reducers.js b/app/reducers.js new file mode 100755 index 0000000..5e289c7 --- /dev/null +++ b/app/reducers.js @@ -0,0 +1,47 @@ +/* Combine all reducers in this file and export the combined reducers. + * If we were to do this in store.js, reducers wouldn't be hot reloadable. + */ + +import { combineReducers } from 'redux-immutable' +import { fromJS } from 'immutable' +import { LOCATION_CHANGE } from 'react-router-redux' +import languageProviderReducer from 'containers/LanguageProvider/reducer' + +/* + * routeReducer + * + * The reducer merges route location changes into our immutable state. + * The change is necessitated by moving to react-router-redux@4 + * + */ + +// Initial routing state +const routeInitialState = fromJS({ + locationBeforeTransitions: null +}) + +/** + * Merge route into the global application state + */ +function routeReducer (state = routeInitialState, action) { + switch (action.type) { + /* istanbul ignore next */ + case LOCATION_CHANGE: + return state.merge({ + locationBeforeTransitions: action.payload + }) + default: + return state + } +} + +/** + * Creates the main reducer with the asynchronously loaded ones + */ +export default function createReducer (asyncReducers) { + return combineReducers({ + route: routeReducer, + language: languageProviderReducer, + ...asyncReducers + }) +} diff --git a/app/routes.js b/app/routes.js new file mode 100755 index 0000000..8ed3e7a --- /dev/null +++ b/app/routes.js @@ -0,0 +1,41 @@ +// import { getHooks } from 'utils/hooks' + +const errorLoading = (err) => { + console.error('Dynamic page loading failed', err) +} + +const loadModule = (cb) => (componentModule) => { + cb(null, componentModule.default) +} + +export default function createRoutes () { + // const { injectReducer, injectSagas } = getHooks(store) + + return [ + { + path: '/', + name: 'home', + getComponent (nextState, cb) { + const importModules = Promise.all([ + System.import('containers/Logger') + ]) + + const renderRoute = loadModule(cb) + + importModules.then(([component]) => { + renderRoute(component) + }) + + importModules.catch(errorLoading) + } + }, { + path: '*', + name: 'notfound', + getComponent (nextState, cb) { + System.import('containers/NotFoundPage') + .then(loadModule(cb)) + .catch(errorLoading) + } + } + ] +} diff --git a/app/store.js b/app/store.js new file mode 100755 index 0000000..d1e20af --- /dev/null +++ b/app/store.js @@ -0,0 +1,49 @@ +/* Create the store with asynchronously loaded reducers */ + +import { createStore, applyMiddleware, compose } from 'redux' +import { fromJS } from 'immutable' +import { routerMiddleware } from 'react-router-redux' +import createSagaMiddleware from 'redux-saga' +import createReducer from './reducers' + +const sagaMiddleware = createSagaMiddleware() +const devtools = window.devToolsExtension || (() => noop => noop) + +export default function configureStore (initialState = {}, history) { + // Create the store with two middlewares + // 1. sagaMiddleware: Makes redux-sagas work + // 2. routerMiddleware: Syncs the location/URL path to the state + const middlewares = [ + sagaMiddleware, + routerMiddleware(history) + ] + + const enhancers = [ + applyMiddleware(...middlewares), + devtools() + ] + + const store = createStore( + createReducer(), + fromJS(initialState), + compose(...enhancers) + ) + + // Create hook for async sagas + store.runSaga = sagaMiddleware.run + + // Make reducers hot reloadable + /* istanbul ignore next */ + if (module.hot) { + System.import('./reducers').then((reducerModule) => { + const createReducers = reducerModule.default + const nextReducers = createReducers(store.asyncReducers) + + store.replaceReducer(nextReducers) + }) + } + + // Initialize it with no other reducers + store.asyncReducers = {} + return store +} diff --git a/app/store.test.js b/app/store.test.js new file mode 100755 index 0000000..bb7c2a4 --- /dev/null +++ b/app/store.test.js @@ -0,0 +1,25 @@ +/* Test store addons */ + +import expect from 'expect' +import configureStore from './store' +import { browserHistory } from 'react-router' + +describe('configureStore', () => { + let store + + before(() => { + store = configureStore({}, browserHistory) + }) + + describe('asyncReducers', () => { + it('should contain an object for async reducers', () => { + expect(typeof store.asyncReducers).toEqual('object') + }) + }) + + describe('runSaga', () => { + it('should contain a hook for `sagaMiddleware.run`', () => { + expect(typeof store.runSaga).toEqual('function') + }) + }) +}) diff --git a/app/tests/store.test.js b/app/tests/store.test.js new file mode 100755 index 0000000..2d9c2f9 --- /dev/null +++ b/app/tests/store.test.js @@ -0,0 +1,25 @@ +/* Test store addons */ + +import expect from 'expect' +import configureStore from '../store' +import { browserHistory } from 'react-router' + +describe('configureStore', () => { + let store + + before(() => { + store = configureStore({}, browserHistory) + }) + + describe('asyncReducers', () => { + it('should contain an object for async reducers', () => { + expect(typeof store.asyncReducers).toEqual('object') + }) + }) + + describe('runSaga', () => { + it('should contain a hook for `sagaMiddleware.run`', () => { + expect(typeof store.runSaga).toEqual('function') + }) + }) +}) diff --git a/app/translations/en.json b/app/translations/en.json new file mode 100755 index 0000000..b28617a --- /dev/null +++ b/app/translations/en.json @@ -0,0 +1,17 @@ +[ + { + "id": "hyperfox.components.NavigationBar.search.message", + "defaultMessage": "Search.", + "message": "" + }, + { + "id": "hyperfox.containers.Logger.Notification.noconnected.message", + "defaultMessage": "Could not connect to the Hyperfox API. Seems like hyperfox is not running on your local box.", + "message": "" + }, + { + "id": "hyperfox.containers.Logger.Notification.nodata.message", + "defaultMessage": "No data to show yet.", + "message": "" + } +] diff --git a/app/translations/es.json b/app/translations/es.json new file mode 100755 index 0000000..7038803 --- /dev/null +++ b/app/translations/es.json @@ -0,0 +1,17 @@ +[ + { + "id": "hyperfox.components.NavigationBar.search.message", + "defaultMessage": "Search.", + "message": "Buscar" + }, + { + "id": "hyperfox.containers.Logger.Notification.noconnected.message", + "defaultMessage": "Could not connect to the Hyperfox API. Seems like hyperfox is not running on your local box.", + "message": "" + }, + { + "id": "hyperfox.containers.Logger.Notification.nodata.message", + "defaultMessage": "No data to show yet.", + "message": "" + } +] \ No newline at end of file diff --git a/app/utils/hooks.js b/app/utils/hooks.js new file mode 100755 index 0000000..f2789b5 --- /dev/null +++ b/app/utils/hooks.js @@ -0,0 +1,22 @@ +import createReducer from 'reducers.js' + +/* Inject an asynchronously loaded reducer */ +export function injectAsyncReducer (store) { + return (name, asyncReducer) => { + store.asyncReducers[name] = asyncReducer + store.replaceReducer(createReducer(store.asyncReducers)) + } +} + +/* Inject an asynchronously loaded saga */ +export function injectAsyncSagas (store) { + return (sagas) => sagas.map(store.runSaga) +} + +/* Helper for creating injectors */ +export function getHooks (store) { + return { + injectReducer: injectAsyncReducer(store), + injectSagas: injectAsyncSagas(store) + } +} diff --git a/app/utils/tests/hooks.test.js b/app/utils/tests/hooks.test.js new file mode 100755 index 0000000..4f0a1c7 --- /dev/null +++ b/app/utils/tests/hooks.test.js @@ -0,0 +1,82 @@ +/* Test hooks */ + +import expect from 'expect' +import configureStore from 'store.js' +import { memoryHistory } from 'react-router' +import { put } from 'redux-saga/effects' +import { fromJS } from 'immutable' + +import {injectAsyncReducer, injectAsyncSagas, getHooks} from 'utils/hooks' + +// Fixtures + +const initialState = fromJS({ reduced: 'soon' }) + +const reducer = (state = initialState, action) => { + switch (action.type) { + case 'TEST': + return state.set('reduced', action.payload) + default: + return state + } +} + +const sagas = [ + function * testSaga () { + yield put({ type: 'TEST', payload: 'yup' }) + } +] + +describe('hooks', () => { + let store + + describe('getHooks', () => { + before(() => { + store = configureStore({}, memoryHistory) + }) + + it('given a store, should return all hooks', () => { + const { injectReducer, injectSagas } = getHooks(store) + + injectReducer('test', reducer) + injectSagas(sagas) + + const actual = store.getState().get('test') + const expected = initialState.merge({ reduced: 'yup' }) + + expect(actual.toJS()).toEqual(expected.toJS()) + }) + }) + + describe('helpers', () => { + before(() => { + store = configureStore({}, memoryHistory) + }) + + describe('injectAsyncReducer', () => { + it('given a store, it should provide a function to inject a reducer', () => { + const injectReducer = injectAsyncReducer(store) + + injectReducer('test', reducer) + + const actual = store.getState().get('test') + const expected = initialState + + expect(actual.toJS()).toEqual(expected.toJS()) + }) + }) + + describe('injectAsyncSagas', () => { + it('given a store, it should provide a function to inject a saga', () => { + const injectSagas = injectAsyncSagas(store) + + injectSagas(sagas) + + const actual = store.getState().get('test') + const expected = initialState.merge({ reduced: 'yup' }) + + expect(actual.toJS()).toEqual(expected.toJS()) + }) + }) + }) +}) diff --git a/electron.js b/electron.js new file mode 100644 index 0000000..546db5f --- /dev/null +++ b/electron.js @@ -0,0 +1,35 @@ +const electron = require('electron') + +const app = electron.app +const BrowserWindow = electron.BrowserWindow + +let mainWindow +function createWindow () { + mainWindow = new BrowserWindow({ + width: 1200, + height: 600, + title: 'Hyperfox', + icon: __dirname + '/electron/hyperfox-icon.ico', + }) + mainWindow.loadURL(`file://${__dirname}/build/index.html`) + mainWindow.on('closed', function () { + mainWindow = null + }) +} + +app.on('ready', createWindow) +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +app.on('activate', function () { + if (mainWindow === null) { + createWindow() + } +}) + +app.on('close-main-window', function () { + app.quit() +}) diff --git a/electron/hyperfox-icon.icns b/electron/hyperfox-icon.icns new file mode 100644 index 0000000..0cf9c02 Binary files /dev/null and b/electron/hyperfox-icon.icns differ diff --git a/electron/hyperfox-icon.ico b/electron/hyperfox-icon.ico new file mode 100644 index 0000000..ee0ac35 Binary files /dev/null and b/electron/hyperfox-icon.ico differ diff --git a/electron/hyperfox-icon.png b/electron/hyperfox-icon.png new file mode 100644 index 0000000..5e03d44 Binary files /dev/null and b/electron/hyperfox-icon.png differ diff --git a/electron/package.js b/electron/package.js new file mode 100644 index 0000000..0e6bf72 --- /dev/null +++ b/electron/package.js @@ -0,0 +1,33 @@ +var packager = require('electron-packager') +var logger = require('../server/logger') + +var platform +if (process.argv.indexOf("-p") !== -1) { + platform = process.argv[process.argv.indexOf('-p') + 1] +} + +var packageOptions = { + dir: './', + name: 'Hyperfox', + platform: platform, + out: './releases', + overwrite: true, + icon: './electron/hyperfox-icon.icns', + ignore: [ + 'app', + 'coverage', + 'releases', + 'internals', + 'server', + 'node_modules' + ] +} + +packager(packageOptions, function done_callback (err, appPaths) { + if (err) { + logger.error(err.message) + } + if (appPaths !== undefined ) { + logger.electron(appPaths) + } +}) diff --git a/html/.bowerrc b/html/.bowerrc deleted file mode 100644 index 69fad35..0000000 --- a/html/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "bower_components" -} diff --git a/html/.editorconfig b/html/.editorconfig deleted file mode 100644 index c2cdfb8..0000000 --- a/html/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# editorconfig.org - -root = true - - -[*] - -# Change these settings to your own preference -indent_style = space -indent_size = 2 - -# We recommend you to keep these unchanged -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/html/.gitattributes b/html/.gitattributes deleted file mode 100644 index 2125666..0000000 --- a/html/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto \ No newline at end of file diff --git a/html/.gitignore b/html/.gitignore deleted file mode 100644 index 980a1aa..0000000 --- a/html/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules -*.swp -dist -.tmp -.sass-cache -bower_components diff --git a/html/.jshintrc b/html/.jshintrc deleted file mode 100644 index f750969..0000000 --- a/html/.jshintrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "single", - "undef": true, - "unused": true, - "strict": true, - "trailing": true, - "smarttabs": true, - "globals": { - "angular": false - } -} diff --git a/html/.travis.yml b/html/.travis.yml deleted file mode 100644 index a80b6e0..0000000 --- a/html/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: node_js -node_js: - - '0.10' -before_script: - - 'npm install -g bower grunt-cli' - - 'bower install' diff --git a/html/Gruntfile.js b/html/Gruntfile.js deleted file mode 100644 index 737d49a..0000000 --- a/html/Gruntfile.js +++ /dev/null @@ -1,441 +0,0 @@ -// Generated on 2014-12-20 using generator-angular 0.10.0 -'use strict'; - -// # Globbing -// for performance reasons we're only matching one level down: -// 'test/spec/{,*/}*.js' -// use this if you want to recursively match all subfolders: -// 'test/spec/**/*.js' - -module.exports = function (grunt) { - - // Load grunt tasks automatically - require('load-grunt-tasks')(grunt); - - // Time how long tasks take. Can help when optimizing build times - require('time-grunt')(grunt); - - // Configurable paths for the application - var appConfig = { - app: require('./bower.json').appPath || 'app', - dist: 'dist' - }; - - // Define the configuration for all the tasks - grunt.initConfig({ - - // Project settings - yeoman: appConfig, - - // Watches files for changes and runs tasks based on the changed files - watch: { - bower: { - files: ['bower.json'], - tasks: ['wiredep'] - }, - js: { - files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], - tasks: ['newer:jshint:all'], - options: { - livereload: '<%= connect.options.livereload %>' - } - }, - jsTest: { - files: ['test/spec/{,*/}*.js'], - tasks: ['newer:jshint:test', 'karma'] - }, - compass: { - files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], - tasks: ['compass:server', 'autoprefixer'] - }, - gruntfile: { - files: ['Gruntfile.js'] - }, - livereload: { - options: { - livereload: '<%= connect.options.livereload %>' - }, - files: [ - '<%= yeoman.app %>/{,*/}*.html', - '.tmp/styles/{,*/}*.css', - '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' - ] - } - }, - - // The actual grunt server settings - connect: { - options: { - port: 9000, - // Change this to '0.0.0.0' to access the server from outside. - hostname: 'localhost', - livereload: 35729 - }, - livereload: { - options: { - open: true, - middleware: function (connect) { - return [ - connect.static('.tmp'), - connect().use( - '/bower_components', - connect.static('./bower_components') - ), - connect.static(appConfig.app) - ]; - } - } - }, - test: { - options: { - port: 9001, - middleware: function (connect) { - return [ - connect.static('.tmp'), - connect.static('test'), - connect().use( - '/bower_components', - connect.static('./bower_components') - ), - connect.static(appConfig.app) - ]; - } - } - }, - dist: { - options: { - open: true, - base: '<%= yeoman.dist %>' - } - } - }, - - // Make sure code styles are up to par and there are no obvious mistakes - jshint: { - options: { - jshintrc: '.jshintrc', - reporter: require('jshint-stylish') - }, - all: { - src: [ - 'Gruntfile.js', - '<%= yeoman.app %>/scripts/{,*/}*.js' - ] - }, - test: { - options: { - jshintrc: 'test/.jshintrc' - }, - src: ['test/spec/{,*/}*.js'] - } - }, - - // Empties folders to start fresh - clean: { - dist: { - files: [{ - dot: true, - src: [ - '.tmp', - '<%= yeoman.dist %>/{,*/}*', - '!<%= yeoman.dist %>/.git{,*/}*' - ] - }] - }, - server: '.tmp' - }, - - // Add vendor prefixed styles - autoprefixer: { - options: { - browsers: ['last 1 version'] - }, - dist: { - files: [{ - expand: true, - cwd: '.tmp/styles/', - src: '{,*/}*.css', - dest: '.tmp/styles/' - }] - } - }, - - // Automatically inject Bower components into the app - wiredep: { - app: { - src: ['<%= yeoman.app %>/index.html'], - ignorePath: /\.\.\// - }, - sass: { - src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], - ignorePath: /(\.\.\/){1,2}bower_components\// - } - }, - - // Compiles Sass to CSS and generates necessary files if requested - compass: { - options: { - sassDir: '<%= yeoman.app %>/styles', - cssDir: '.tmp/styles', - generatedImagesDir: '.tmp/images/generated', - imagesDir: '<%= yeoman.app %>/images', - javascriptsDir: '<%= yeoman.app %>/scripts', - fontsDir: '<%= yeoman.app %>/styles/fonts', - importPath: './bower_components', - httpImagesPath: '/images', - httpGeneratedImagesPath: '/images/generated', - httpFontsPath: '/styles/fonts', - relativeAssets: false, - assetCacheBuster: false, - raw: 'Sass::Script::Number.precision = 10\n' - }, - dist: { - options: { - generatedImagesDir: '<%= yeoman.dist %>/images/generated' - } - }, - server: { - options: { - debugInfo: true - } - } - }, - - // Renames files for browser caching purposes - filerev: { - dist: { - src: [ - '<%= yeoman.dist %>/scripts/{,*/}*.js', - '<%= yeoman.dist %>/styles/{,*/}*.css', - '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', - '<%= yeoman.dist %>/styles/fonts/*' - ] - } - }, - - // Reads HTML for usemin blocks to enable smart builds that automatically - // concat, minify and revision files. Creates configurations in memory so - // additional tasks can operate on them - useminPrepare: { - html: '<%= yeoman.app %>/index.html', - options: { - dest: '<%= yeoman.dist %>', - flow: { - html: { - steps: { - js: ['concat', 'uglifyjs'], - css: ['cssmin'] - }, - post: {} - } - } - } - }, - - // Performs rewrites based on filerev and the useminPrepare configuration - usemin: { - html: ['<%= yeoman.dist %>/{,*/}*.html'], - css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], - options: { - assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images'] - } - }, - - // The following *-min tasks will produce minified files in the dist folder - // By default, your `index.html`'s will take care of - // minification. These next options are pre-configured if you do not wish - // to use the Usemin blocks. - // cssmin: { - // dist: { - // files: { - // '<%= yeoman.dist %>/styles/main.css': [ - // '.tmp/styles/{,*/}*.css' - // ] - // } - // } - // }, - // uglify: { - // dist: { - // files: { - // '<%= yeoman.dist %>/scripts/scripts.js': [ - // '<%= yeoman.dist %>/scripts/scripts.js' - // ] - // } - // } - // }, - // concat: { - // dist: {} - // }, - - imagemin: { - dist: { - files: [{ - expand: true, - cwd: '<%= yeoman.app %>/images', - src: '{,*/}*.{png,jpg,jpeg,gif}', - dest: '<%= yeoman.dist %>/images' - }] - } - }, - - svgmin: { - dist: { - files: [{ - expand: true, - cwd: '<%= yeoman.app %>/images', - src: '{,*/}*.svg', - dest: '<%= yeoman.dist %>/images' - }] - } - }, - - htmlmin: { - dist: { - options: { - collapseWhitespace: true, - conservativeCollapse: true, - collapseBooleanAttributes: true, - removeCommentsFromCDATA: true, - removeOptionalTags: true - }, - files: [{ - expand: true, - cwd: '<%= yeoman.dist %>', - src: ['*.html', 'views/{,*/}*.html'], - dest: '<%= yeoman.dist %>' - }] - } - }, - - // ng-annotate tries to make the code safe for minification automatically - // by using the Angular long form for dependency injection. - ngAnnotate: { - dist: { - files: [{ - expand: true, - cwd: '.tmp/concat/scripts', - src: ['*.js', '!oldieshim.js'], - dest: '.tmp/concat/scripts' - }] - } - }, - - // Replace Google CDN references - cdnify: { - dist: { - html: ['<%= yeoman.dist %>/*.html'] - } - }, - - // Copies remaining files to places other tasks can use - copy: { - dist: { - files: [{ - expand: true, - dot: true, - cwd: '<%= yeoman.app %>', - dest: '<%= yeoman.dist %>', - src: [ - '*.{ico,png,txt}', - '.htaccess', - '*.html', - 'views/{,*/}*.html', - 'images/{,*/}*.{webp}', - 'fonts/{,*/}*.*' - ] - }, { - expand: true, - cwd: '.tmp/images', - dest: '<%= yeoman.dist %>/images', - src: ['generated/*'] - }, { - expand: true, - cwd: '.', - src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*', - dest: '<%= yeoman.dist %>' - }] - }, - styles: { - expand: true, - cwd: '<%= yeoman.app %>/styles', - dest: '.tmp/styles/', - src: '{,*/}*.css' - } - }, - - // Run some tasks in parallel to speed up the build process - concurrent: { - server: [ - 'compass:server' - ], - test: [ - 'compass' - ], - dist: [ - 'compass:dist', - 'imagemin', - 'svgmin' - ] - }, - - // Test settings - karma: { - unit: { - configFile: 'test/karma.conf.js', - singleRun: true - } - } - }); - - - grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { - if (target === 'dist') { - return grunt.task.run(['build', 'connect:dist:keepalive']); - } - - grunt.task.run([ - 'clean:server', - 'wiredep', - 'concurrent:server', - 'autoprefixer', - 'connect:livereload', - 'watch' - ]); - }); - - grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { - grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); - grunt.task.run(['serve:' + target]); - }); - - grunt.registerTask('test', [ - 'clean:server', - 'concurrent:test', - 'autoprefixer', - 'connect:test', - 'karma' - ]); - - grunt.registerTask('build', [ - 'clean:dist', - 'wiredep', - 'useminPrepare', - 'concurrent:dist', - 'autoprefixer', - 'concat', - 'ngAnnotate', - 'copy:dist', - 'cdnify', - 'cssmin', - 'uglify', - 'filerev', - 'usemin', - 'htmlmin' - ]); - - grunt.registerTask('default', [ - 'newer:jshint', - 'test', - 'build' - ]); -}; diff --git a/html/Makefile b/html/Makefile deleted file mode 100644 index 64337a5..0000000 --- a/html/Makefile +++ /dev/null @@ -1,6 +0,0 @@ -clean: - grunt clean - -publish: clean - grunt build - scp -r dist/* www-data@hyperfox.org:/var/www/hyperfox.org/live/ diff --git a/html/app/.buildignore b/html/app/.buildignore deleted file mode 100644 index fc98b8e..0000000 --- a/html/app/.buildignore +++ /dev/null @@ -1 +0,0 @@ -*.coffee \ No newline at end of file diff --git a/html/app/.htaccess b/html/app/.htaccess deleted file mode 100644 index cb84cb9..0000000 --- a/html/app/.htaccess +++ /dev/null @@ -1,543 +0,0 @@ -# Apache Configuration File - -# (!) Using `.htaccess` files slows down Apache, therefore, if you have access -# to the main server config file (usually called `httpd.conf`), you should add -# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html. - -# ############################################################################## -# # CROSS-ORIGIN RESOURCE SHARING (CORS) # -# ############################################################################## - -# ------------------------------------------------------------------------------ -# | Cross-domain AJAX requests | -# ------------------------------------------------------------------------------ - -# Enable cross-origin AJAX requests. -# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity -# http://enable-cors.org/ - -# -# Header set Access-Control-Allow-Origin "*" -# - -# ------------------------------------------------------------------------------ -# | CORS-enabled images | -# ------------------------------------------------------------------------------ - -# Send the CORS header for images when browsers request it. -# https://developer.mozilla.org/en/CORS_Enabled_Image -# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html -# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/ - - - - - SetEnvIf Origin ":" IS_CORS - Header set Access-Control-Allow-Origin "*" env=IS_CORS - - - - -# ------------------------------------------------------------------------------ -# | Web fonts access | -# ------------------------------------------------------------------------------ - -# Allow access from all domains for web fonts - - - - Header set Access-Control-Allow-Origin "*" - - - - -# ############################################################################## -# # ERRORS # -# ############################################################################## - -# ------------------------------------------------------------------------------ -# | 404 error prevention for non-existing redirected folders | -# ------------------------------------------------------------------------------ - -# Prevent Apache from returning a 404 error for a rewrite if a directory -# with the same name does not exist. -# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews -# http://www.webmasterworld.com/apache/3808792.htm - -Options -MultiViews - -# ------------------------------------------------------------------------------ -# | Custom error messages / pages | -# ------------------------------------------------------------------------------ - -# You can customize what Apache returns to the client in case of an error (see -# http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.: - -ErrorDocument 404 /404.html - - -# ############################################################################## -# # INTERNET EXPLORER # -# ############################################################################## - -# ------------------------------------------------------------------------------ -# | Better website experience | -# ------------------------------------------------------------------------------ - -# Force IE to render pages in the highest available mode in the various -# cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf. - - - Header set X-UA-Compatible "IE=edge" - # `mod_headers` can't match based on the content-type, however, we only - # want to send this header for HTML pages and not for the other resources - - Header unset X-UA-Compatible - - - -# ------------------------------------------------------------------------------ -# | Cookie setting from iframes | -# ------------------------------------------------------------------------------ - -# Allow cookies to be set from iframes in IE. - -# -# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\"" -# - -# ------------------------------------------------------------------------------ -# | Screen flicker | -# ------------------------------------------------------------------------------ - -# Stop screen flicker in IE on CSS rollovers (this only works in -# combination with the `ExpiresByType` directives for images from below). - -# BrowserMatch "MSIE" brokenvary=1 -# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1 -# BrowserMatch "Opera" !brokenvary -# SetEnvIf brokenvary 1 force-no-vary - - -# ############################################################################## -# # MIME TYPES AND ENCODING # -# ############################################################################## - -# ------------------------------------------------------------------------------ -# | Proper MIME types for all files | -# ------------------------------------------------------------------------------ - - - - # Audio - AddType audio/mp4 m4a f4a f4b - AddType audio/ogg oga ogg - - # JavaScript - # Normalize to standard type (it's sniffed in IE anyways): - # http://tools.ietf.org/html/rfc4329#section-7.2 - AddType application/javascript js jsonp - AddType application/json json - - # Video - AddType video/mp4 mp4 m4v f4v f4p - AddType video/ogg ogv - AddType video/webm webm - AddType video/x-flv flv - - # Web fonts - AddType application/font-woff woff - AddType application/vnd.ms-fontobject eot - - # Browsers usually ignore the font MIME types and sniff the content, - # however, Chrome shows a warning if other MIME types are used for the - # following fonts. - AddType application/x-font-ttf ttc ttf - AddType font/opentype otf - - # Make SVGZ fonts work on iPad: - # https://twitter.com/FontSquirrel/status/14855840545 - AddType image/svg+xml svg svgz - AddEncoding gzip svgz - - # Other - AddType application/octet-stream safariextz - AddType application/x-chrome-extension crx - AddType application/x-opera-extension oex - AddType application/x-shockwave-flash swf - AddType application/x-web-app-manifest+json webapp - AddType application/x-xpinstall xpi - AddType application/xml atom rdf rss xml - AddType image/webp webp - AddType image/x-icon ico - AddType text/cache-manifest appcache manifest - AddType text/vtt vtt - AddType text/x-component htc - AddType text/x-vcard vcf - - - -# ------------------------------------------------------------------------------ -# | UTF-8 encoding | -# ------------------------------------------------------------------------------ - -# Use UTF-8 encoding for anything served as `text/html` or `text/plain`. -AddDefaultCharset utf-8 - -# Force UTF-8 for certain file formats. - - AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml - - - -# ############################################################################## -# # URL REWRITES # -# ############################################################################## - -# ------------------------------------------------------------------------------ -# | Rewrite engine | -# ------------------------------------------------------------------------------ - -# Turning on the rewrite engine and enabling the `FollowSymLinks` option is -# necessary for the following directives to work. - -# If your web host doesn't allow the `FollowSymlinks` option, you may need to -# comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the -# performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks - -# Also, some cloud hosting services require `RewriteBase` to be set: -# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site - - - Options +FollowSymlinks - # Options +SymLinksIfOwnerMatch - RewriteEngine On - # RewriteBase / - - -# ------------------------------------------------------------------------------ -# | Suppressing / Forcing the "www." at the beginning of URLs | -# ------------------------------------------------------------------------------ - -# The same content should never be available under two different URLs especially -# not with and without "www." at the beginning. This can cause SEO problems -# (duplicate content), therefore, you should choose one of the alternatives and -# redirect the other one. - -# By default option 1 (no "www.") is activated: -# http://no-www.org/faq.php?q=class_b - -# If you'd prefer to use option 2, just comment out all the lines from option 1 -# and uncomment the ones from option 2. - -# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME! - -# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# Option 1: rewrite www.example.com → example.com - - - RewriteCond %{HTTPS} !=on - RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] - RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] - - -# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# Option 2: rewrite example.com → www.example.com - -# Be aware that the following might not be a good idea if you use "real" -# subdomains for certain parts of your website. - -# -# RewriteCond %{HTTPS} !=on -# RewriteCond %{HTTP_HOST} !^www\..+$ [NC] -# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] -# - - -# ############################################################################## -# # SECURITY # -# ############################################################################## - -# ------------------------------------------------------------------------------ -# | Content Security Policy (CSP) | -# ------------------------------------------------------------------------------ - -# You can mitigate the risk of cross-site scripting and other content-injection -# attacks by setting a Content Security Policy which whitelists trusted sources -# of content for your site. - -# The example header below allows ONLY scripts that are loaded from the current -# site's origin (no inline scripts, no CDN, etc). This almost certainly won't -# work as-is for your site! - -# To get all the details you'll need to craft a reasonable policy for your site, -# read: http://html5rocks.com/en/tutorials/security/content-security-policy (or -# see the specification: http://w3.org/TR/CSP). - -# -# Header set Content-Security-Policy "script-src 'self'; object-src 'self'" -# -# Header unset Content-Security-Policy -# -# - -# ------------------------------------------------------------------------------ -# | File access | -# ------------------------------------------------------------------------------ - -# Block access to directories without a default document. -# Usually you should leave this uncommented because you shouldn't allow anyone -# to surf through every directory on your server (which may includes rather -# private places like the CMS's directories). - - - Options -Indexes - - -# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# Block access to hidden files and directories. -# This includes directories used by version control systems such as Git and SVN. - - - RewriteCond %{SCRIPT_FILENAME} -d [OR] - RewriteCond %{SCRIPT_FILENAME} -f - RewriteRule "(^|/)\." - [F] - - -# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# Block access to backup and source files. -# These files may be left by some text editors and can pose a great security -# danger when anyone has access to them. - - - Order allow,deny - Deny from all - Satisfy All - - -# ------------------------------------------------------------------------------ -# | Secure Sockets Layer (SSL) | -# ------------------------------------------------------------------------------ - -# Rewrite secure requests properly to prevent SSL certificate warnings, e.g.: -# prevent `https://www.example.com` when your certificate only allows -# `https://secure.example.com`. - -# -# RewriteCond %{SERVER_PORT} !^443 -# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L] -# - -# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -# Force client-side SSL redirection. - -# If a user types "example.com" in his browser, the above rule will redirect him -# to the secure version of the site. That still leaves a window of opportunity -# (the initial HTTP connection) for an attacker to downgrade or redirect the -# request. The following header ensures that browser will ONLY connect to your -# server via HTTPS, regardless of what the users type in the address bar. -# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/ - -# -# Header set Strict-Transport-Security max-age=16070400; -# - -# ------------------------------------------------------------------------------ -# | Server software information | -# ------------------------------------------------------------------------------ - -# Avoid displaying the exact Apache version number, the description of the -# generic OS-type and the information about Apache's compiled-in modules. - -# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`! - -# ServerTokens Prod - - -# ############################################################################## -# # WEB PERFORMANCE # -# ############################################################################## - -# ------------------------------------------------------------------------------ -# | Compression | -# ------------------------------------------------------------------------------ - - - - # Force compression for mangled headers. - # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping - - - SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding - RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding - - - - # Compress all output labeled with one of the following MIME-types - # (for Apache versions below 2.3.7, you don't need to enable `mod_filter` - # and can remove the `` and `` lines - # as `AddOutputFilterByType` is still in the core directives). - - AddOutputFilterByType DEFLATE application/atom+xml \ - application/javascript \ - application/json \ - application/rss+xml \ - application/vnd.ms-fontobject \ - application/x-font-ttf \ - application/x-web-app-manifest+json \ - application/xhtml+xml \ - application/xml \ - font/opentype \ - image/svg+xml \ - image/x-icon \ - text/css \ - text/html \ - text/plain \ - text/x-component \ - text/xml - - - - -# ------------------------------------------------------------------------------ -# | Content transformations | -# ------------------------------------------------------------------------------ - -# Prevent some of the mobile network providers from modifying the content of -# your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5. - -# -# Header set Cache-Control "no-transform" -# - -# ------------------------------------------------------------------------------ -# | ETag removal | -# ------------------------------------------------------------------------------ - -# Since we're sending far-future expires headers (see below), ETags can -# be removed: http://developer.yahoo.com/performance/rules.html#etags. - -# `FileETag None` is not enough for every server. - - Header unset ETag - - -FileETag None - -# ------------------------------------------------------------------------------ -# | Expires headers (for better cache control) | -# ------------------------------------------------------------------------------ - -# The following expires headers are set pretty far in the future. If you don't -# control versioning with filename-based cache busting, consider lowering the -# cache time for resources like CSS and JS to something like 1 week. - - - - ExpiresActive on - ExpiresDefault "access plus 1 month" - - # CSS - ExpiresByType text/css "access plus 1 year" - - # Data interchange - ExpiresByType application/json "access plus 0 seconds" - ExpiresByType application/xml "access plus 0 seconds" - ExpiresByType text/xml "access plus 0 seconds" - - # Favicon (cannot be renamed!) - ExpiresByType image/x-icon "access plus 1 week" - - # HTML components (HTCs) - ExpiresByType text/x-component "access plus 1 month" - - # HTML - ExpiresByType text/html "access plus 0 seconds" - - # JavaScript - ExpiresByType application/javascript "access plus 1 year" - - # Manifest files - ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds" - ExpiresByType text/cache-manifest "access plus 0 seconds" - - # Media - ExpiresByType audio/ogg "access plus 1 month" - ExpiresByType image/gif "access plus 1 month" - ExpiresByType image/jpeg "access plus 1 month" - ExpiresByType image/png "access plus 1 month" - ExpiresByType video/mp4 "access plus 1 month" - ExpiresByType video/ogg "access plus 1 month" - ExpiresByType video/webm "access plus 1 month" - - # Web feeds - ExpiresByType application/atom+xml "access plus 1 hour" - ExpiresByType application/rss+xml "access plus 1 hour" - - # Web fonts - ExpiresByType application/font-woff "access plus 1 month" - ExpiresByType application/vnd.ms-fontobject "access plus 1 month" - ExpiresByType application/x-font-ttf "access plus 1 month" - ExpiresByType font/opentype "access plus 1 month" - ExpiresByType image/svg+xml "access plus 1 month" - - - -# ------------------------------------------------------------------------------ -# | Filename-based cache busting | -# ------------------------------------------------------------------------------ - -# If you're not using a build process to manage your filename version revving, -# you might want to consider enabling the following directives to route all -# requests such as `/css/style.12345.css` to `/css/style.css`. - -# To understand why this is important and a better idea than `*.css?v231`, read: -# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring - -# -# RewriteCond %{REQUEST_FILENAME} !-f -# RewriteCond %{REQUEST_FILENAME} !-d -# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L] -# - -# ------------------------------------------------------------------------------ -# | File concatenation | -# ------------------------------------------------------------------------------ - -# Allow concatenation from within specific CSS and JS files, e.g.: -# Inside of `script.combined.js` you could have -# -# -# and they would be included into this single file. - -# -# -# Options +Includes -# AddOutputFilterByType INCLUDES application/javascript application/json -# SetOutputFilter INCLUDES -# -# -# Options +Includes -# AddOutputFilterByType INCLUDES text/css -# SetOutputFilter INCLUDES -# -# - -# ------------------------------------------------------------------------------ -# | Persistent connections | -# ------------------------------------------------------------------------------ - -# Allow multiple requests to be sent over the same TCP connection: -# http://httpd.apache.org/docs/current/en/mod/core.html#keepalive. - -# Enable if you serve a lot of static content but, be aware of the -# possible disadvantages! - -# -# Header set Connection Keep-Alive -# diff --git a/html/app/favicon.ico b/html/app/favicon.ico deleted file mode 100644 index 372714a..0000000 Binary files a/html/app/favicon.ico and /dev/null differ diff --git a/html/app/images/logo.svg b/html/app/images/logo.svg deleted file mode 100644 index 31e11ac..0000000 --- a/html/app/images/logo.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/html/app/index.html b/html/app/index.html deleted file mode 100644 index 6aa9128..0000000 --- a/html/app/index.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - hyperfox live - - - - - - - - - - - - - - - - - - -
- - - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/html/app/scripts/app.js b/html/app/scripts/app.js deleted file mode 100644 index 8e7acad..0000000 --- a/html/app/scripts/app.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -/** - * @ngdoc overview - * @name webApp - * @description - * # webApp - * - * Main module of the application. - */ -angular - .module('webApp', [ - 'ngAnimate', - 'ngCookies', - 'ngResource', - 'ngRoute', - 'ngSanitize', - 'ngTouch' - //'luegg.directives' - ]) - .config(function ($routeProvider) { - $routeProvider - .when('/details/:id', { - templateUrl: 'views/details.html', - controller: 'DetailsCtrl' - }) - .when('/:page?', { - templateUrl: 'views/main.html', - controller: 'MainCtrl' - }) - .otherwise({ - redirectTo: '/#/' - }); - }) - .directive('tooltip', function(){ - return { - restrict: 'A', - link: function(scope, element, attrs) { - $(element).hover(function(){ - // on mouseenter - $(element).tooltip('show'); - }, function(){ - // on mouseleave - $(element).tooltip('hide'); - }); - } - }; - }); - ; diff --git a/html/app/scripts/controllers/main.js b/html/app/scripts/controllers/main.js deleted file mode 100644 index 85f3d96..0000000 --- a/html/app/scripts/controllers/main.js +++ /dev/null @@ -1,142 +0,0 @@ -'use strict'; - -var ERR_HYPERFOX_NOT_RUNNING = 'Could not connect to the Hyperfox API. Seems like hyperfox is not running on your local box.'; - -/** - * @ngdoc function - * @name webApp.controller:MainCtrl - * @description - * # MainCtrl - * Controller of the webApp - */ -angular.module('webApp') - .controller('ParentCtrl', function($scope, $routeParams, api) { - - $scope.apierror = null; - - $scope.setTerms = function() { - $scope.$broadcast('search'); - }; - - }) - .controller('DetailsCtrl', function($scope, $routeParams, api) { - $scope.currentID = parseInt($routeParams.id, 10); - $scope.details = {}; - - api.get($scope.currentID).then(function(res) { - $scope.apierror = null; - - $scope.details = res; - }, function(reason) { - $scope.apierror = ERR_HYPERFOX_NOT_RUNNING; - }); - - $scope.$on('search', function() { - location.href = '/#/1'; - refresh(); - }); - - }) - .controller('MainCtrl', function($scope, $routeParams, api) { - - if ($routeParams.source) { - window.localStorage.setItem('source', $routeParams.source); - }; - - $scope.$on('search', function() { - location.href = '/#/'; - refresh(); - }); - - $scope.requests = []; - $scope.totalPages = 0; - $scope.currentPage = parseInt($routeParams.page || 1, 10); - - var pull = function(page, q) { - api.pull(page, q).then(function(res) { - $scope.apierror = null; - - $scope.requests = res.data; - $scope.totalPages = res.pages; - $scope.currentPage = res.page; - - }, function(reason) { - $scope.apierror = ERR_HYPERFOX_NOT_RUNNING; - }); - }; - - var refresh = function() { - pull($scope.currentPage, $scope.q); - }; - - setInterval( - function() { - refresh(); - }, - 10000 - ); - - refresh(); - }) - .filter('mime', function() { - return function(input) { - var c = input.indexOf(';'); - if (c >= 0) { - return input.substr(0, c); - }; - return input; - } - }) - .filter('origin', function() { - return function(input) { - if (input) { - var c = input.indexOf(':'); - if (c >= 0) { - return input.substr(0, c); - }; - } - return input; - } - }) - .filter('size', function() { - return function(input) { - if (!input) { - return '-'; - }; - var kb = 1024; - if (input < kb) { - return input + 'b'; - } else if (input < kb*kb) { - return (input/kb).toFixed(2) + 'Kb'; - } - return (input/(kb*kb)).toFixed(2) + 'Mb'; - } - }) - .filter('time', function() { - return function(input) { - if (input < 1e9) { - return (input/1e6).toFixed(2) + 'ms'; - }; - return (input/1e9).toFixed(2) + 's'; - } - }) - .filter('api_request', function() { - return function(input) { - return endpoint(input); - } - }) - .filter('pageRange', function() { - return function(input, current, total, size) { - var sz = 3; - - var lo = Math.max(1, current - sz); - var hi = Math.min(total, current + sz); - - var i; - for (i = lo; i <= hi; i++) { - input.push(i); - }; - - return input; - }; - }); diff --git a/html/app/scripts/services/main.js b/html/app/scripts/services/main.js deleted file mode 100644 index 8f2b071..0000000 --- a/html/app/scripts/services/main.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -function endpoint(url) { - var prefix = window.localStorage.getItem('source'); - - if (!prefix) { - prefix = '127.0.0.1:3030'; - }; - - return 'http://' + prefix + url; -} - -var downloadLink = function(id) { - return endpoint('/download?id='+id); -}; - -angular.module('webApp') - .service('api', function($http) { - var api = {}; - - api.get = function(id) { - var req = { - 'method': 'POST', - 'url': endpoint('/get'), - 'data': $.param({ - 'id': id - }), - 'headers': { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }; - - var promise = $http(req).then(function(res) { - return res.data; - }); - - return promise; - }; - - api.pull = function(page, q) { - - var req = { - 'method': 'POST', - 'url': endpoint('/pull'), - 'data': $.param({ - 'page': page, - 'q': q || '' - }), - 'headers': { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }; - - var promise = $http(req).then(function(res) { - return res.data; - }); - - return promise; - }; - - return api; - }); diff --git a/html/app/styles/main.scss b/html/app/styles/main.scss deleted file mode 100644 index 34c40ff..0000000 --- a/html/app/styles/main.scss +++ /dev/null @@ -1,197 +0,0 @@ -$icon-font-path: "../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/"; -// bower:scss -@import "bootstrap-sass-official/assets/stylesheets/_bootstrap.scss"; -// endbower - -.browsehappy { - margin: 0.2em 0; - background: #ccc; - color: #000; - padding: 0.2em 0; -} - -/* Space out content a bit */ -body { - padding-top: 70px; - padding-bottom: 20px; -} - -/* Everything but the jumbotron gets side spacing for mobile first views */ -.header, -.marketing, -.footer { - padding-left: 15px; - padding-right: 15px; -} - -/* Custom page header */ -.header { - border-bottom: 1px solid #e5e5e5; - - /* Make the masthead heading the same height as the navigation */ - h3 { - margin-top: 0; - margin-bottom: 0; - line-height: 40px; - padding-bottom: 19px; - } -} - -/* Custom page footer */ -.footer { - padding-top: 19px; - color: #777; - border-top: 1px solid #e5e5e5; -} - -.container-narrow > hr { - margin: 30px 0; -} - -/* Main marketing message and sign up button */ -.jumbotron { - text-align: center; - border-bottom: 1px solid #e5e5e5; - - .btn { - font-size: 21px; - padding: 14px 24px; - } -} - -/* Supporting marketing content */ -.marketing { - margin: 40px 0; - - p + h4 { - margin-top: 28px; - } -} - -/* Responsive: Portrait tablets and up */ -@media screen and (min-width: 768px) { - .container { - max-width: 730px; - } - - /* Remove the padding we set earlier */ - .header, - .marketing, - .footer { - padding-left: 0; - padding-right: 0; - } - /* Space out the masthead */ - .header { - margin-bottom: 30px; - } - /* Remove the bottom border on the jumbotron for visual effect */ - .jumbotron { - border-bottom: 0; - } -} -#requests-paginator { - margin-left: 10px; - margin-right: 10px; - position: fixed; - bottom: 10px; -} -#requests-paginator ul { - margin: 0px; - padding: 0px; -} -#details-wrapper { - height: auto; -} -#details-wrapper dd { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.container { - max-width: 100%; -} -.data-rows { - height: auto; - overflow-y: scroll; - position: fixed; - top: 50px; - left: 0px; - right: 0px; - bottom: 65px; -} -.content-fixed { - position: fixed; - top: 0px; - left: 0px; - bottom: 0px; - right: 0px; -} -.content-fluid { - width: 100%; -} -#requests-wrapper th { - text-align: center; -} - -#requests-wrapper th.th-scheme { - width: 80px; -} -#requests-wrapper th.th-path { - width: 300px; -} -#requests-wrapper th.th-type { - width: 120px; -} -#requests-wrapper th.th-direction { - width: 100px; -} -#requests-wrapper th.th-host { - width: 200px; -} -#requests-wrapper th.th-method { - width: 80px; -} -#requests-wrapper th.th-origin { - width: 120px; -} -#requests-wrapper tbody td { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -#requests-wrapper td.td-path { - max-width: 300px; -} -#requests-wrapper td.td-host { - max-width: 200px; -} -#requests-wrapper td.td-scheme { - text-align: center; -} -#requests-wrapper td.td-type { - max-width: 120px; - text-align: center; -} -#requests-wrapper td.td-origin { - max-width: 150px; -} -#requests-wrapper td.td-method { - text-align: center; -} -#requests-wrapper td.td-status { - text-align: center; -} -#requests-wrapper td.td-size { - text-align: center; -} -#requests-wrapper td.td-actions { - text-align: center; -} -.navbar-brand img { - display: inline; -} -.buttons { - text-align: right; -} diff --git a/html/app/views/details.html b/html/app/views/details.html deleted file mode 100644 index 213b50f..0000000 --- a/html/app/views/details.html +++ /dev/null @@ -1,70 +0,0 @@ -
-
- - - -
- -

{{details.origin | origin}} -> {{details.host}}

- -

Abstract

- -

- On {{details.date_start | date: 'MMM d, y H:mm:ss'}} a - {{details.method}} request was issued to {{details.host}} by - {{details.origin | origin}}. -

-

- The request had a size of {{details.content_length | size}} and took - {{details.time_taken | time}} to complete. -

-

- The request took {{details.time_taken | time}} to complete. -

- -
-

- -
- -
-
-
- - -
- - -

- - Server response -

- -
- - -
{{header}}
-
{{value}}
-
-
-
- -

- - Client request -

- -
- - -
{{header}}
-
{{value}}
-
-
-
-
- -
-
diff --git a/html/app/views/main.html b/html/app/views/main.html deleted file mode 100644 index 1e2b241..0000000 --- a/html/app/views/main.html +++ /dev/null @@ -1,108 +0,0 @@ - - - -
- -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- diff --git a/html/bower.json b/html/bower.json deleted file mode 100644 index f33a5b7..0000000 --- a/html/bower.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "web", - "version": "0.0.0", - "dependencies": { - "angular": "^1.3.0", - "json3": "^3.3.0", - "es5-shim": "^4.0.0", - "bootstrap-sass-official": "^3.2.0", - "angular-animate": "^1.3.0", - "angular-cookies": "^1.3.0", - "angular-resource": "^1.3.0", - "angular-route": "^1.3.0", - "angular-sanitize": "^1.3.0", - "angular-touch": "^1.3.0", - "angularjs-scroll-glue-Luegg": "~2.0.3" - }, - "devDependencies": { - "angular-mocks": "~1.3.0", - "angular-scenario": "~1.3.0" - }, - "appPath": "app" -} diff --git a/html/package.json b/html/package.json deleted file mode 100644 index 03d9e6b..0000000 --- a/html/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "web", - "version": "0.0.0", - "dependencies": { - "grunt": "~0.4.5" - }, - "devDependencies": { - "grunt": "^0.4.1", - "grunt-autoprefixer": "^0.7.3", - "grunt-concurrent": "^0.5.0", - "grunt-contrib-clean": "^0.5.0", - "grunt-contrib-compass": "~1.0.1", - "grunt-contrib-concat": "^0.4.0", - "grunt-contrib-connect": "^0.7.1", - "grunt-contrib-copy": "^0.5.0", - "grunt-contrib-cssmin": "^0.9.0", - "grunt-contrib-htmlmin": "^0.3.0", - "grunt-contrib-imagemin": "^0.8.1", - "grunt-contrib-jshint": "^0.10.0", - "grunt-contrib-uglify": "^0.4.0", - "grunt-contrib-watch": "^0.6.1", - "grunt-filerev": "^0.2.1", - "grunt-google-cdn": "^0.4.0", - "grunt-karma": "^0.9.0", - "grunt-newer": "^0.7.0", - "grunt-ng-annotate": "^0.4.0", - "grunt-svgmin": "^0.4.0", - "grunt-usemin": "^2.1.1", - "grunt-wiredep": "^1.7.0", - "jasmine-core": "^2.1.3", - "jshint-stylish": "^0.2.0", - "karma": "^0.12.28", - "karma-jasmine": "^0.3.2", - "karma-phantomjs-launcher": "^0.1.4", - "load-grunt-tasks": "^0.4.0", - "time-grunt": "^0.3.1" - }, - "engines": { - "node": ">=0.10.0" - }, - "scripts": { - "test": "grunt test" - } -} diff --git a/html/test/.jshintrc b/html/test/.jshintrc deleted file mode 100644 index b1be025..0000000 --- a/html/test/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "undef": true, - "unused": true, - "strict": true, - "trailing": true, - "smarttabs": true, - "globals": { - "after": false, - "afterEach": false, - "angular": false, - "before": false, - "beforeEach": false, - "browser": false, - "describe": false, - "expect": false, - "inject": false, - "it": false, - "jasmine": false, - "spyOn": false - } -} - diff --git a/html/test/karma.conf.js b/html/test/karma.conf.js deleted file mode 100644 index 5242282..0000000 --- a/html/test/karma.conf.js +++ /dev/null @@ -1,75 +0,0 @@ -// Karma configuration -// http://karma-runner.github.io/0.12/config/configuration-file.html -// Generated on 2014-12-20 using -// generator-karma 0.8.3 - -module.exports = function(config) { - 'use strict'; - - config.set({ - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - // base path, that will be used to resolve files and exclude - basePath: '../', - - // testing framework to use (jasmine/mocha/qunit/...) - frameworks: ['jasmine'], - - // list of files / patterns to load in the browser - files: [ - 'bower_components/angular/angular.js', - 'bower_components/angular-mocks/angular-mocks.js', - 'bower_components/angular-animate/angular-animate.js', - 'bower_components/angular-cookies/angular-cookies.js', - 'bower_components/angular-resource/angular-resource.js', - 'bower_components/angular-route/angular-route.js', - 'bower_components/angular-sanitize/angular-sanitize.js', - 'bower_components/angular-touch/angular-touch.js', - 'app/scripts/**/*.js', - 'test/mock/**/*.js', - 'test/spec/**/*.js' - ], - - // list of files / patterns to exclude - exclude: [], - - // web server port - port: 8080, - - // Start these browsers, currently available: - // - Chrome - // - ChromeCanary - // - Firefox - // - Opera - // - Safari (only Mac) - // - PhantomJS - // - IE (only Windows) - browsers: [ - 'PhantomJS' - ], - - // Which plugins to enable - plugins: [ - 'karma-phantomjs-launcher', - 'karma-jasmine' - ], - - // Continuous Integration mode - // if true, it capture browsers, run tests and exit - singleRun: false, - - colors: true, - - // level of logging - // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG - logLevel: config.LOG_INFO, - - // Uncomment the following lines if you are using grunt's server to run the tests - // proxies: { - // '/': 'http://localhost:9000/' - // }, - // URL root prevent conflicts with the site root - // urlRoot: '_karma_' - }); -}; diff --git a/html/test/spec/controllers/about.js b/html/test/spec/controllers/about.js deleted file mode 100644 index 2366696..0000000 --- a/html/test/spec/controllers/about.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -describe('Controller: AboutCtrl', function () { - - // load the controller's module - beforeEach(module('webApp')); - - var AboutCtrl, - scope; - - // Initialize the controller and a mock scope - beforeEach(inject(function ($controller, $rootScope) { - scope = $rootScope.$new(); - AboutCtrl = $controller('AboutCtrl', { - $scope: scope - }); - })); - - it('should attach a list of awesomeThings to the scope', function () { - expect(scope.awesomeThings.length).toBe(3); - }); -}); diff --git a/html/test/spec/controllers/main.js b/html/test/spec/controllers/main.js deleted file mode 100644 index 482025b..0000000 --- a/html/test/spec/controllers/main.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -describe('Controller: MainCtrl', function () { - - // load the controller's module - beforeEach(module('webApp')); - - var MainCtrl, - scope; - - // Initialize the controller and a mock scope - beforeEach(inject(function ($controller, $rootScope) { - scope = $rootScope.$new(); - MainCtrl = $controller('MainCtrl', { - $scope: scope - }); - })); - - it('should attach a list of awesomeThings to the scope', function () { - expect(scope.awesomeThings.length).toBe(3); - }); -}); diff --git a/internals/config.js b/internals/config.js new file mode 100755 index 0000000..1c87061 --- /dev/null +++ b/internals/config.js @@ -0,0 +1,56 @@ +const resolve = require('path').resolve +const pullAll = require('lodash/pullAll') +const uniq = require('lodash/uniq') + +const HyperfoxLive = { + // This refers to the hyperfox-live version this project is based on. + version: '2.0.0', + + /** + * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading + * by caching the module metadata for all of our npm dependencies. We enable it by default + * in development. + * + * + * To disable the DLL Plugin, set this value to false. + */ + dllPlugin: { + defaults: { + /** + * we need to exclude dependencies which are not intended for the browser + * by listing them here. + */ + exclude: [ + 'chalk', + 'compression', + 'cross-env', + 'express', + 'minimist', + 'sanitize.css', + 'bulma' + ], + + /** + * Specify any additional dependencies here. We include core-js and lodash + * since a lot of our dependencies depend on them and they get picked up by webpack. + */ + include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'], + + // The path where the DLL manifest and bundle will get built + path: resolve('../node_modules/hyperfox-dlls') + }, + + entry (pkg) { + const dependencyNames = Object.keys(pkg.dependencies) + const exclude = pkg.dllPlugin.exclude || HyperfoxLive.dllPlugin.defaults.exclude + const include = pkg.dllPlugin.include || HyperfoxLive.dllPlugin.defaults.include + const includeDependencies = uniq(dependencyNames.concat(include)) + + return { + hyperfoxLiveDeps: pullAll(includeDependencies, exclude) + } + } + } +} + +module.exports = HyperfoxLive diff --git a/internals/scripts/dependencies.js b/internals/scripts/dependencies.js new file mode 100755 index 0000000..50b62a5 --- /dev/null +++ b/internals/scripts/dependencies.js @@ -0,0 +1,49 @@ +/*eslint-disable*/ + +// No need to build the DLL in production +if (process.env.NODE_ENV === 'production') { + process.exit(0) +} + +require('shelljs/global') + +const path = require('path') +const fs = require('fs') +const exists = fs.existsSync +const writeFile = fs.writeFileSync + +const defaults = require('lodash/defaultsDeep') +const pkg = require(path.join(process.cwd(), 'package.json')) +const config = require('../config') +const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults) +const outputPath = path.join(process.cwd(), dllConfig.path) +const dllManifestPath = path.join(outputPath, 'package.json') + +/** + * I use node_modules/hyperfox-dlls by default just because + * it isn't going to be version controlled and babel wont try to parse it. + */ +mkdir('-p', outputPath) + +echo('Building the Webpack DLL...') + +/** + * Create a manifest so npm install doesnt warn us + */ +if (!exists(dllManifestPath)) { + writeFile( + dllManifestPath, + JSON.stringify(defaults({ + name: 'hyperfox-dlls', + private: true, + author: pkg.author, + repository: pkg.repository, + version: pkg.version + }), null, 2), + + 'utf8' + ) +} + +// the BUILDING_DLL env var is set to avoid confusing the development environment +exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js') diff --git a/internals/scripts/getTranslations.js b/internals/scripts/getTranslations.js new file mode 100644 index 0000000..c166303 --- /dev/null +++ b/internals/scripts/getTranslations.js @@ -0,0 +1,130 @@ +/** + * This script will extract the internationalization messages from all components + and package them in the transalation json files in the translations file. + */ +const fs = require('fs') +const nodeGlob = require('glob') +const chalk = require('chalk') +const transform = require('babel-core').transform +const pkg = require('../../package.json') +const i18n = require('../../app/i18n') + +require('shelljs/global') + +// Glob to match all js files except test files +const FILES_TO_PARSE = 'app/**/!(*.test).js' +const locales = i18n.appLocales + +// Wrap async functions below into a promise +const glob = (pattern) => new Promise((resolve, reject) => { + nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value))) +}) + +const readFile = (fileName) => new Promise((resolve, reject) => { + fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value))) +}) + +const writeFile = (fileName, data) => new Promise((resolve, reject) => { + fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value))) +}) + +// Helper function to log a gray dividing line +const logDivider = () => { + console.log(chalk.gray('------------------------------------------------------------------------')) +} + +// Helper function to log an error in bold red +const logError = (error, ...args) => { + console.error(chalk.bold.red(error), ...args) +} + +// Helper function to log a success message in bold green +const logSuccess = (success, ...args) => { + console.log(chalk.green(success), ...args) +} + +// Store existing translations into memory +const oldLocaleMappings = [] +const localeMappings = [] +// Loop to run once per locale +for (const locale of locales) { + oldLocaleMappings[locale] = {} + localeMappings[locale] = {} + // File to store translation messages into + const translationFileName = `app/translations/${locale}.json` + try { + // Parse the old translation message JSON files + const messages = JSON.parse(fs.readFileSync(translationFileName)) + for (const message of messages) { + oldLocaleMappings[locale][message.id] = message + } + } catch (error) { + if (error.code !== 'ENOENT') { + logError(`There was an error loading this translation file: ${translationFileName}\n${error}`) + } + } +} + +const extractFromFile = async (fileName) => { + try { + const code = await readFile(fileName) + // Use babel plugin to extract instances where react-intl is used + const { metadata: result } = await transform(code, { + presets: pkg.babel.presets, + plugins: [['react-intl']] + }) + for (const message of result['react-intl'].messages) { + for (const locale of locales) { + const oldLocaleMapping = oldLocaleMappings[locale][message.id] + // Merge old translations into the babel extracted instances where react-intl is used + localeMappings[locale][message.id] = { + id: message.id, + description: message.description, + defaultMessage: message.defaultMessage, + message: (oldLocaleMapping && oldLocaleMapping.message) ? oldLocaleMapping.message : '' + } + } + } + } catch (error) { + logError(`There was an error transforming this file: ${fileName}\n${error}`) + } +}; + +(async function main () { + const files = await glob(FILES_TO_PARSE) + + // Run extraction on all files that match the glob on line 16 + await Promise.all( + files.map((fileName) => extractFromFile(fileName)) + ) + + // Make the directory if it doesn't exist, especially for first run + mkdir('-p', 'app/translations') + logDivider() + for (const locale of locales) { + const translationFileName = `app/translations/${locale}.json` + try { + // Sort the translation JSON file so that git diffing is easier + // Otherwise the translation messages will jump around every time we extract + let messages = Object.values(localeMappings[locale]).sort((a, b) => { + a = a.id.toUpperCase() + b = b.id.toUpperCase() + if (a < b) { + return -1 + } else if (a > b) { + return 1 + } else { + return 0 + } + }) + // Write to file the JSON representation of the translation messages + await writeFile(translationFileName, JSON.stringify(messages, null, 2)) + + logSuccess(`Translation file for ${locale} successfully saved to: ${translationFileName}`) + } catch (error) { + logError(`There was an error saving this translation file: ${translationFileName}\n${error}`) + } + } + + logDivider() +}()) diff --git a/internals/testing/karma.conf.js b/internals/testing/karma.conf.js new file mode 100755 index 0000000..caeb989 --- /dev/null +++ b/internals/testing/karma.conf.js @@ -0,0 +1,58 @@ +const webpackConfig = require('../webpack/webpack.test.babel') +const argv = require('minimist')(process.argv.slice(2)) +const path = require('path') + +module.exports = (config) => { + config.set({ + frameworks: ['mocha'], + reporters: ['coverage', 'mocha'], + browsers: process.env.TRAVIS ? ['ChromeTravis'] : process.env.APPVEYOR ? ['IE'] : ['Chrome'], + + autoWatch: false, + singleRun: true, + + client: { + mocha: { + grep: argv.grep + } + }, + + files: [ + { + pattern: './test-bundler.js', + watched: false, + served: true, + included: true + } + ], + + preprocessors: { + ['./test-bundler.js']: ['webpack', 'sourcemap'] + }, + + webpack: webpackConfig, + + // make Webpack bundle generation quiet + webpackMiddleware: { + noInfo: true, + stats: 'errors-only' + }, + + customLaunchers: { + ChromeTravis: { + base: 'Chrome', + flags: ['--no-sandbox'] + } + }, + + coverageReporter: { + dir: path.join(process.cwd(), 'coverage'), + reporters: [ + { type: 'lcov', subdir: 'lcov' }, + { type: 'html', subdir: 'html' }, + { type: 'text-summary' } + ] + } + + }) +} diff --git a/internals/testing/test-bundler.js b/internals/testing/test-bundler.js new file mode 100755 index 0000000..2471ed7 --- /dev/null +++ b/internals/testing/test-bundler.js @@ -0,0 +1,12 @@ +// needed for regenerator-runtime +// (ES7 generator support is required by redux-saga) +import 'babel-polyfill' + +import chai from 'chai' +import chaiEnzyme from 'chai-enzyme' +chai.use(chaiEnzyme()) + +// Include all .js files under `app`, except app.js, reducers.js, routes.js and +// store.js. This is for isparta code coverage +const context = require.context('../../app', true, /^^((?!(app|reducers|routes|store)).)*\.js$/) +context.keys().forEach(context) diff --git a/internals/webpack/webpack.base.babel.js b/internals/webpack/webpack.base.babel.js new file mode 100755 index 0000000..cec3896 --- /dev/null +++ b/internals/webpack/webpack.base.babel.js @@ -0,0 +1,85 @@ +/* COMMON WEBPACK CONFIGURATION */ + +const path = require('path') +const webpack = require('webpack') + +module.exports = (options) => ({ + entry: options.entry, + output: Object.assign({ // Compile into js/build.js + path: path.resolve(process.cwd(), 'build'), + publicPath: '/' + }, options.output), // Merge with env dependent settings + module: { + loaders: [{ + test: /\.js$/, // Transform all .js files required somewhere with Babel + loader: 'babel', + exclude: /node_modules/, + query: options.babelQuery + }, { + // Transform our own .css files with PostCSS and CSS-modules + test: /\.css$/, + exclude: /node_modules/, + loader: options.cssLoaders + }, { + // Do not transform vendor's CSS with CSS-modules + // The point is that they remain in global scope. + // Since we require these CSS files in our JS or CSS files, + // they will be a part of our compilation either way. + // So, no need for ExtractTextPlugin here. + test: /\.css$/, + include: /node_modules/, + loaders: ['style-loader', 'css-loader'] + }, { + test: /\.(eot|svg|ttf|woff|woff2)$/, + loader: 'file-loader' + }, { + test: /\.(jpg|png|gif)$/, + loaders: [ + 'file-loader', + 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}' + ] + }, { + test: /\.html$/, + loader: 'html-loader' + }, { + test: /\.json$/, + loader: 'json-loader' + }, { + test: /\.(mp4|webm)$/, + loader: 'url-loader?limit=10000' + }] + }, + plugins: options.plugins.concat([ + new webpack.ProvidePlugin({ + // make fetch available + fetch: 'exports?self.fetch!whatwg-fetch' + }), + + // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` + // inside your code for any environment checks; UglifyJS will automatically + // drop any unreachable code. + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(process.env.NODE_ENV) + } + }) + ]), + postcss: () => options.postcssPlugins, + resolve: { + modules: ['app', 'node_modules'], + extensions: [ + '', + '.js', + '.jsx', + '.react.js' + ], + packageMains: [ + 'jsnext:main', + 'main' + ] + }, + devtool: options.devtool, + target: 'web', // Make web variables accessible to webpack, e.g. window + stats: false, // Don't show stats in the console + progress: true +}) diff --git a/internals/webpack/webpack.dev.babel.js b/internals/webpack/webpack.dev.babel.js new file mode 100755 index 0000000..19641d6 --- /dev/null +++ b/internals/webpack/webpack.dev.babel.js @@ -0,0 +1,143 @@ +/* DEVELOPMENT WEBPACK CONFIGURATION */ + +const path = require('path') +const fs = require('fs') +const webpack = require('webpack') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const logger = require('../../server/logger') +const cheerio = require('cheerio') +const pkg = require(path.resolve(process.cwd(), 'package.json')) +const dllPlugin = pkg.dllPlugin + +// PostCSS plugins +const cssnext = require('postcss-cssnext') +const postcssFocus = require('postcss-focus') +const postcssReporter = require('postcss-reporter') + +const plugins = [ + new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading + new webpack.NoErrorsPlugin(), + new HtmlWebpackPlugin({ + inject: true, // Inject all files that are generated by webpack, e.g. bundle.js + templateContent: templateContent() + }) +] + +module.exports = require('./webpack.base.babel')({ + // Add hot reloading in development + entry: [ + 'eventsource-polyfill', // Necessary for hot reloading with IE + 'webpack-hot-middleware/client', + path.join(process.cwd(), 'app/app.js') // Start with js/app.js + ], + + // Don't use hashes in dev mode for better performance + output: { + filename: '[name].js', + chunkFilename: '[name].chunk.js' + }, + + // Add development plugins + plugins: dependencyHandlers().concat(plugins), + + // Load the CSS in a style tag in development + cssLoaders: 'style-loader!css-loader?localIdentName=[local]__[path][name]__[hash:base64:5]&modules&importLoaders=1&sourceMap!postcss-loader', + + // Process the CSS with PostCSS + postcssPlugins: [ + require('precss')(), + postcssFocus(), // Add a :focus to every :hover + cssnext({ // Allow future CSS features to be used, also auto-prefixes the CSS... + browsers: ['last 2 versions', 'IE > 10'] // ...based on this browser list + }), + postcssReporter({ // Posts messages from plugins to the terminal + clearMessages: true + }) + ], + + // Tell babel that we want to hot-reload + babelQuery: { + presets: ['react-hmre'] + }, + + // Emit a source map for easier debugging + devtool: 'cheap-module-eval-source-map' +}) + +/** + * Select which plugins to use to optimize the bundle's handling of + * third party dependencies. + * + * If there is a dllPlugin key on the project's package.json, the + * Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin + * will be used. + * + */ +function dependencyHandlers () { + // Don't do anything during the DLL Build step + if (process.env.BUILDING_DLL) { return [] } + + // If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin + if (!dllPlugin) { + return [ + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + children: true, + minChunks: 2, + async: true + }) + ] + } + + const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/hyperfox-dlls') + if (!dllPlugin.dlls) { + const manifestPath = path.resolve(dllPath, 'hyperfoxLiveDeps.json') + + if (!fs.existsSync(manifestPath)) { + logger.error('The DLL manifest is missing. Please run `npm run build:dll`') + process.exit(0) + } + + return [ + new webpack.DllReferencePlugin({ + context: process.cwd(), + manifest: require(manifestPath) + }) + ] + } + + const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`)) + + return dllManifests.map((manifestPath) => { + if (!fs.existsSync(path)) { + if (!fs.existsSync(manifestPath)) { + logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`) + logger.error(`Expected to find it in ${dllPath}`) + logger.error('Please run: npm run build:dll') + + process.exit(0) + } + } + + return new webpack.DllReferencePlugin({ + context: process.cwd(), + manifest: require(manifestPath) + }) + }) +} + +function templateContent () { + const html = fs.readFileSync( + path.resolve(process.cwd(), 'app/index.html') + ).toString() + + if (!dllPlugin) { return html } + + const doc = cheerio(html) + const body = doc.find('body') + const dllNames = !dllPlugin.dlls ? ['hyperfoxLiveDeps'] : Object.keys(dllPlugin.dlls) + + dllNames.forEach(dllName => body.append(``)) + + return doc.toString() +} diff --git a/internals/webpack/webpack.dll.babel.js b/internals/webpack/webpack.dll.babel.js new file mode 100755 index 0000000..a835685 --- /dev/null +++ b/internals/webpack/webpack.dll.babel.js @@ -0,0 +1,34 @@ +/** + * WEBPACK DLL GENERATOR + * + * This profile is used to cache webpack's module + * contexts for external library and framework type + * dependencies which will usually not change often enough + * to warrant building them from scratch every time we use + * the webpack process. + */ + +const { join } = require('path') +const defaults = require('lodash/defaultsDeep') +const webpack = require('webpack') +const pkg = require(join(process.cwd(), 'package.json')) +const dllPlugin = require('../config').dllPlugin + +if (!pkg.dllPlugin) { process.exit(0) } + +const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults) +const outputPath = join(process.cwd(), dllConfig.path) + +module.exports = { + context: process.cwd(), + entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), + devtool: 'eval', + output: { + filename: '[name].dll.js', + path: outputPath, + library: '[name]' + }, + plugins: [ + new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }) + ] +} diff --git a/internals/webpack/webpack.prod.babel.js b/internals/webpack/webpack.prod.babel.js new file mode 100755 index 0000000..797f2fb --- /dev/null +++ b/internals/webpack/webpack.prod.babel.js @@ -0,0 +1,110 @@ +const path = require('path') +const webpack = require('webpack') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const OfflinePlugin = require('offline-plugin') + +// PostCSS plugins +const cssnext = require('postcss-cssnext') +const postcssFocus = require('postcss-focus') +const postcssReporter = require('postcss-reporter') + +module.exports = require('./webpack.base.babel')({ + // In production, we skip all hot-reloading stuff + entry: [ + path.join(process.cwd(), 'app/app.js') + ], + + // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets + output: { + filename: '[name].[chunkhash].js', + chunkFilename: '[name].[chunkhash].chunk.js', + publicPath: `file://${path.join(process.cwd(), 'build/')}` + }, + + // We use ExtractTextPlugin so we get a seperate CSS file instead + // of the CSS being in the JS and injected as a style tag + cssLoaders: ExtractTextPlugin.extract({ + fallbackLoader: 'style-loader', + loader: 'css-loader?modules&importLoaders=1!postcss-loader' + }), + + // In production, we minify our CSS with cssnano + postcssPlugins: [ + require('precss')(), + postcssFocus(), + cssnext({ + browsers: ['last 2 versions', 'IE > 10'] + }), + postcssReporter({ + clearMessages: true + }) + ], + plugins: [ + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + children: true, + minChunks: 2, + async: true + }), + + // OccurrenceOrderPlugin is needed for long-term caching to work properly. + new webpack.optimize.OccurrenceOrderPlugin(true), + + // Merge all duplicate modules + new webpack.optimize.DedupePlugin(), + + // Minify and optimize the JavaScript + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false // ...but do not show warnings in the console (there is a lot of them) + } + }), + + // Minify and optimize the index.html + new HtmlWebpackPlugin({ + template: 'app/index.html', + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true + }, + inject: true + }), + + // Extract the CSS into a seperate file + new ExtractTextPlugin('[name].[contenthash].css'), + + // Put it in the end to capture all the HtmlWebpackPlugin's + // assets manipulations and do leak its manipulations to HtmlWebpackPlugin + new OfflinePlugin({ + // No need to cache .htaccess. + excludes: ['.htaccess'], + + caches: { + main: [':rest:'], + + // All chunks marked as `additional`, loaded after main section + // and do not prevent SW to install. Change to `optional` if + // do not want them to be preloaded at all (cached only when first loaded) + additional: ['*.chunk.js'] + }, + + // Removes warning for about `additional` section usage + safeToUseOptionalCaches: true, + + AppCache: { + // Starting from offline-plugin:v3, AppCache by default caches only + // `main` section. This lets it use `additional` section too + caches: ['main', 'additional'] + } + }) + ] +}) diff --git a/internals/webpack/webpack.test.babel.js b/internals/webpack/webpack.test.babel.js new file mode 100755 index 0000000..3d7e31c --- /dev/null +++ b/internals/webpack/webpack.test.babel.js @@ -0,0 +1,88 @@ +/* TEST WEBPACK CONFIGURATION */ + +const path = require('path') +const webpack = require('webpack') +const modules = [ + 'app', + 'node_modules' +] + +module.exports = { + devtool: 'inline-source-map', + isparta: { + babel: { + presets: ['es2015', 'react', 'stage-0'] + } + }, + module: { + // Some libraries don't like being run through babel. + // If they gripe, put them here. + noParse: [ + /node_modules(\\|\/)sinon/, + /node_modules(\\|\/)acorn/ + ], + preLoaders: [ + { test: /\.js$/, + loader: 'isparta', + include: path.resolve('app/') + } + ], + loaders: [ + { test: /\.json$/, loader: 'json-loader' }, + { test: /\.css$/, loader: 'null-loader' }, + + // sinon.js--aliased for enzyme--expects/requires global vars. + // imports-loader allows for global vars to be injected into the module. + // See https://github.com/webpack/webpack/issues/304 + { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, + loader: 'imports?define=>false,require=>false' + }, + { test: /\.js$/, + loader: 'babel', + exclude: [/node_modules/] + }, + { test: /\.jpe?g$|\.gif$|\.png$/i, + loader: 'null-loader' + } + ] + }, + + plugins: [ + + // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` + // inside your code for any environment checks; UglifyJS will automatically + // drop any unreachable code. + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify(process.env.NODE_ENV) + } + })], + + // Some node_modules pull in Node-specific dependencies. + // Since we're running in a browser we have to stub them out. See: + // https://webpack.github.io/docs/configuration.html#node + // https://github.com/webpack/node-libs-browser/tree/master/mock + // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 + node: { + fs: 'empty', + child_process: 'empty', + net: 'empty', + tls: 'empty' + }, + + // required for enzyme to work properly + externals: { + jsdom: 'window', + 'react/addons': true, + 'react/lib/ExecutionEnvironment': true, + 'react/lib/ReactContext': 'window' + }, + resolve: { + modulesDirectories: modules, + modules, + alias: { + // required for enzyme to work properly + sinon: 'sinon/pkg/sinon' + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f9419fa --- /dev/null +++ b/package.json @@ -0,0 +1,176 @@ +{ + "name": "hyperfox-live", + "version": "2.0.0", + "description": "", + "main": "electron.js", + "repository": { + "type": "git", + "url": "git://github.com/xiam/hyperfox-live.git" + }, + "engines": { + "npm": ">=3" + }, + "scripts": { + "postinstall": "npm run build:dll", + "intl": "babel-node --presets es2015 -- ./internals/scripts/getTranslations.js", + "prebuild": "rimraf ./build/*", + "build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.babel.js --color -p", + "build:dll": "node ./internals/scripts/dependencies.js", + "prepackage": "npm run build", + "package": "node ./electron/package.js -p all", + "package:linux": "npm run prepackage && node ./electron/package.js -p linux", + "package:osx": "npm run prepackage && node ./electron/package.js -p darwin", + "package:win": "npm run prepackage && node ./electron/package.js -p win32", + "start": "electron ./electron.js", + "start:development": "cross-env NODE_ENV=development node server", + "start:production": "npm run build && npm run start:prod", + "start:prod": "cross-env NODE_ENV=production node server", + "test": "cross-env NODE_ENV=test karma start internals/testing/karma.conf.js --single-run", + "test:watch": "npm run test -- --auto-watch --no-single-run", + "test:firefox": "npm run test -- --browsers Firefox", + "test:safari": "npm run test -- --browsers Safari", + "test:ie": "npm run test -- --browsers IE", + "coveralls": "cat ./coverage/lcov/lcov.info | coveralls" + }, + "babel": { + "presets": [ + [ + "es2015", + { + "modules": false + } + ], + "react", + "stage-0" + ], + "env": { + "production": { + "only": [ + "app" + ], + "plugins": [ + "transform-react-remove-prop-types", + "transform-react-constant-elements", + "transform-react-inline-elements" + ] + } + } + }, + "dllPlugin": { + "path": "node_modules/hyperfox-dlls", + "exclude": [ + "chalk", + "compression", + "cross-env", + "express", + "minimist", + "sanitize.css", + "bulma" + ], + "include": [ + "core-js", + "lodash", + "eventsource-polyfill" + ] + }, + "dependencies": { + "babel-polyfill": "6.13.0", + "bulma": "0.0.28", + "chalk": "^1.1.3", + "classnames": "^2.2.5", + "compression": "^1.6.1", + "express": "4.14.0", + "fontfaceobserver": "2.0.4", + "history": "^2.1.0", + "immutable": "^3.8.1", + "intl": "^1.2.4", + "minimist": "^1.2.0", + "react": "15.3.1", + "react-dom": "15.3.1", + "react-intl": "^2.1.5", + "react-redux": "^4.4.5", + "react-router": "^2.8.0", + "react-router-redux": "^4.0.5", + "react-router-scroll": "^0.3.2", + "redux": "3.6.0", + "redux-immutable": "^3.0.8", + "redux-saga": "^0.11.1", + "reselect": "^2.5.3", + "sanitize.css": "^4.1.0", + "whatwg-fetch": "^1.0.0" + }, + "devDependencies": { + "babel-cli": "6.14.0", + "babel-core": "6.14.0", + "babel-eslint": "6.1.2", + "babel-loader": "6.2.5", + "babel-plugin-react-intl": "2.2.0", + "babel-plugin-react-transform": "2.0.2", + "babel-plugin-transform-react-constant-elements": "6.9.1", + "babel-plugin-transform-react-inline-elements": "6.8.0", + "babel-plugin-transform-react-remove-prop-types": "0.2.9", + "babel-preset-es2015": "6.14.0", + "babel-preset-react": "6.11.1", + "babel-preset-react-hmre": "^1.1.1", + "babel-preset-stage-0": "^6.5.0", + "chai": "3.5.0", + "chai-enzyme": "0.5.1", + "cheerio": "^0.22.0", + "coveralls": "^2.11.12", + "cross-env": "2.0.1", + "css-loader": "0.25.0", + "electron": "^1.4.1", + "electron-packager": "^8.0.0", + "enzyme": "^2.4.1", + "eventsource-polyfill": "^0.9.6", + "expect": "1.20.2", + "expect-jsx": "2.6.0", + "exports-loader": "^0.6.3", + "extract-text-webpack-plugin": "2.0.0-beta.4", + "file-loader": "0.9.0", + "html-loader": "^0.4.3", + "html-webpack-plugin": "2.22.0", + "image-webpack-loader": "2.0.0", + "imports-loader": "^0.6.5", + "json-loader": "0.5.4", + "karma": "1.3.0", + "karma-chrome-launcher": "2.0.0", + "karma-coverage": "1.1.1", + "karma-firefox-launcher": "1.0.0", + "karma-ie-launcher": "1.0.0", + "karma-mocha": "1.1.1", + "karma-mocha-reporter": "2.1.0", + "karma-safari-launcher": "1.0.0", + "karma-sourcemap-loader": "0.3.7", + "karma-webpack": "1.8.0", + "lodash": "^4.13.1", + "mocha": "3.0.2", + "null-loader": "^0.1.1", + "offline-plugin": "3.4.2", + "postcss-cssnext": "2.8.0", + "postcss-focus": "1.0.0", + "postcss-loader": "0.13.0", + "postcss-reporter": "1.4.1", + "precss": "^1.4.0", + "rimraf": "^2.5.4", + "shelljs": "^0.7.4", + "sinon": "^2.0.0-pre", + "style-loader": "^0.13.1", + "url-loader": "^0.5.7", + "webpack": "2.1.0-beta.22", + "webpack-dev-middleware": "1.7.0", + "webpack-hot-middleware": "2.12.2" + }, + "standard": { + "parser": "babel-eslint", + "globals": [ + "describe", + "it", + "fetch", + "before", + "beforeEach", + "afterEach", + "Response" + ] + } +} diff --git a/server/index.js b/server/index.js new file mode 100755 index 0000000..3392e85 --- /dev/null +++ b/server/index.js @@ -0,0 +1,26 @@ +const express = require('express') +const logger = require('./logger') + +const argv = require('minimist')(process.argv.slice(2)) +const setup = require('./middlewares/frontendMiddleware') +const resolve = require('path').resolve +const app = express() + +// app.use('/api', myApi) + +// In production we need to pass these values in instead of relying on webpack +setup(app, { + outputPath: resolve(process.cwd(), 'build'), + publicPath: '/' +}) + +// get the intended port number, use port 3000 if not provided +const port = argv.port || process.env.PORT || 3000 + +app.listen(port, (err) => { + if (err) { + return logger.error(err.message) + } + + logger.appStarted(port) +}) diff --git a/server/logger.js b/server/logger.js new file mode 100755 index 0000000..75c56a1 --- /dev/null +++ b/server/logger.js @@ -0,0 +1,19 @@ +const chalk = require('chalk') +const divider = chalk.gray('\n-----------------------------------') +const logger = { + error: err => { + console.error(chalk.red(err)) + }, + + appStarted: (port) => { + console.log(`Server started ${chalk.green('✓')}`) + console.log(`${divider}\nLocalhost: ${chalk.magenta(`http://localhost:${port}`)}${divider}`) + }, + + electron: (dirs) => { + console.log(`App build with electron ${chalk.green('✓')}`) + console.log(`Saved on: ${chalk.magenta(`${dirs}`)}`) + } +} + +module.exports = logger diff --git a/server/middlewares/frontendMiddleware.js b/server/middlewares/frontendMiddleware.js new file mode 100755 index 0000000..d9a67dc --- /dev/null +++ b/server/middlewares/frontendMiddleware.js @@ -0,0 +1,67 @@ +const express = require('express') +const path = require('path') +const compression = require('compression') +const pkg = require(path.resolve(process.cwd(), 'package.json')) + +// Dev middleware +const addDevMiddlewares = (app, webpackConfig) => { + const webpack = require('webpack') + const webpackDevMiddleware = require('webpack-dev-middleware') + const webpackHotMiddleware = require('webpack-hot-middleware') + const compiler = webpack(webpackConfig) + const middleware = webpackDevMiddleware(compiler, { + noInfo: true, + publicPath: webpackConfig.output.publicPath, + silent: true, + stats: 'errors-only' + }) + + app.use(middleware) + app.use(webpackHotMiddleware(compiler)) + + // Since webpackDevMiddleware uses memory-fs internally to store build artifacts, we use it instead + const fs = middleware.fileSystem + + if (pkg.dllPlugin) { + app.get(/\.dll\.js$/, (req, res) => { + const filename = req.path.replace(/^\//, '') + res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename)) + }) + } + + app.get('*', (req, res) => { + fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { + if (err) { + res.sendStatus(404) + } else { + res.send(file.toString()) + } + }) + }) +} + +// Production middlewares +const addProdMiddlewares = (app, options) => { + const publicPath = options.publicPath || '/' + const outputPath = options.outputPath || path.resolve(process.cwd(), 'build') + + // compression middleware compresses server responses + app.use(compression()) + app.use(publicPath, express.static(outputPath)) + + app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html'))) +} + +// Front-end middleware +module.exports = (app, options) => { + const isProd = process.env.NODE_ENV === 'production' + + if (isProd) { + addProdMiddlewares(app, options) + } else { + const webpackConfig = require('../../internals/webpack/webpack.dev.babel') + addDevMiddlewares(app, webpackConfig) + } + + return app +}