├── .env ├── src ├── static │ ├── robots.txt │ ├── favicon.ico │ └── humans.txt ├── styles │ ├── components │ │ ├── App.js │ │ ├── ThemeSelector.js │ │ ├── Author.js │ │ ├── IndexSidebar.js │ │ ├── Calculate.js │ │ ├── all.js │ │ ├── Button.js │ │ ├── Calculation.js │ │ ├── CalculationsList.js │ │ ├── _mediaQueries.js │ │ └── Index.js │ ├── themes │ │ ├── dark.js │ │ ├── light.js │ │ ├── base.js │ │ └── createTheme.js │ ├── index.js │ ├── layout │ │ └── base.js │ ├── variables.js │ └── globalStyles.js ├── containers │ ├── DevToolsWindow.js │ ├── DevTools.js │ ├── App.js │ ├── ThemeManager.js │ ├── Root.js │ ├── IndexSidebar.js │ └── Index.js ├── routes │ └── index.js ├── redux │ ├── rootReducer.js │ ├── modules │ │ ├── events.js │ │ ├── settings.js │ │ ├── themes.js │ │ ├── keys.js │ │ └── calculations.js │ ├── configureStore.js │ ├── utils │ │ └── createDevToolsWindow.js │ ├── middleware │ │ ├── toggleButton.js │ │ └── handleEvents.js │ └── selectors.js ├── initialState.js ├── keyboardLayouts │ └── basicArithmetic.js ├── index.html ├── main.js ├── components │ ├── Button.js │ ├── Author.js │ ├── ThemeSelector.js │ ├── CalculationsList.js │ ├── Calculation.js │ ├── Calculate.js │ └── Flex.js └── helpers │ └── pureFunctions.js ├── jsconfig.json ├── CHANGELOG.md ├── .eslintignore ├── .gitignore ├── bin ├── karma.js ├── server.js └── compile.js ├── nodemon.json ├── .babelrc ├── tests ├── test-helpers │ ├── createCalculation.js │ ├── render.js │ └── shouldComponentUpdate.js ├── .eslintrc ├── framework.spec.js ├── redux │ ├── modules │ │ ├── events.spec.js │ │ ├── settings.spec.js │ │ ├── themes.spec.js │ │ ├── keys.spec.js │ │ └── calculations.spec.js │ ├── middleware │ │ ├── toggleButton.spec.js │ │ └── handleEvents.spec.js │ └── selectors.spec.js ├── test-bundler.js ├── containers │ ├── ThemeManager.spec.js │ ├── App.spec.js │ ├── IndexSidebar.spec.js │ └── Index.spec.js ├── components │ ├── ThemeSelector.spec.js │ ├── Author.spec.js │ ├── Button.spec.js │ ├── CalculationsList.spec.js │ ├── Calculation.spec.js │ └── Calculate.spec.js └── helpers │ └── pureFunctions.spec.js ├── .travis.yml ├── config ├── _development.js ├── _production.js ├── index.js └── _base.js ├── gulpfile.babel.js ├── server ├── lib │ └── apply-express-middleware.js ├── middleware │ ├── webpack-hmr.js │ └── webpack-dev.js └── main.js ├── .codeclimate.yml ├── .editorconfig ├── LICENSE ├── TODO.md ├── README.md ├── .eslintrc └── package.json /.env: -------------------------------------------------------------------------------- 1 | DEBUG=app:* 2 | BLUEBIRD_WARNINGS=0 3 | -------------------------------------------------------------------------------- /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.1.0 5 | ----- 6 | 7 | #### Not there yet 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/** 2 | node_modules/** 3 | dist/** 4 | *.spec.js 5 | src/index.html 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | 4 | node_modules 5 | 6 | dist 7 | coverage 8 | .publish 9 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/panayi/calculator/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /bin/karma.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | 3 | module.exports = require('../build/karma.conf') 4 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["dist", "coverage", "tests", "src"] 4 | } 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": ["typecheck", "transform-runtime", "add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /tests/test-helpers/createCalculation.js: -------------------------------------------------------------------------------- 1 | export default function (input, output, isError = false) { 2 | return { 3 | input, 4 | output, 5 | isError 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/components/App.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.app__sidebar': { 4 | width: variables.layout.sidebarWidth 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/components/ThemeSelector.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.theme-selector__icon': { 4 | fontSize: variables.fontSizes.button 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/containers/DevToolsWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | 5 | export default createDevTools( 6 | 7 | ) 8 | -------------------------------------------------------------------------------- /src/styles/themes/dark.js: -------------------------------------------------------------------------------- 1 | export default { 2 | primaryColor: '#16a085', 3 | accentColor: '#FAAB22', 4 | accent2Color: '#EE213D', 5 | canvasColor: '#27313E', 6 | primaryContrastColor: 'white', 7 | accentContrastColor: 'white' 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/themes/light.js: -------------------------------------------------------------------------------- 1 | export default { 2 | primaryColor: '#0A6987', 3 | accentColor: '#DA1415', 4 | accent2Color: '#363B40', 5 | canvasColor: '#DADADA', 6 | primaryContrastColor: 'white', 7 | accentContrastColor: 'white' 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "5.0" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm run lint 15 | - npm run test 16 | - gulp deploy 17 | -------------------------------------------------------------------------------- /src/static/humans.txt: -------------------------------------------------------------------------------- 1 | /* TEAM */ 2 | Developer: Panagiotis Panagi 3 | Twitter: @ppanagi 4 | From: Limassol, Cyprus 5 | 6 | /* SITE */ 7 | Last update: 2015/01/13 8 | Language: English 9 | Doctype: HTML5 10 | Components: React, Redux, Ramda 11 | IDE: Atom 12 | -------------------------------------------------------------------------------- /config/_development.js: -------------------------------------------------------------------------------- 1 | // We use an explicit public path when the assets are served by webpack 2 | // to fix this issue: 3 | // http://stackoverflow.com/a/34133809/359104 4 | export default (config) => ({ 5 | compiler_public_path: `http://${config.server_host}:${config.server_port}/` 6 | }) 7 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "globals": { 7 | "$": false, 8 | "expect": false, 9 | "should": false, 10 | "sinon": false, 11 | "render": false, 12 | "shallowRender": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/components/Author.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.author__github': { 4 | minWidth: '16px', 5 | minHeight: '16px' 6 | }, 7 | '.author__tweet': { 8 | minWidth: '68px', 9 | minHeight: '29px' 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/_production.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 */ 2 | export default (config) => ({ 3 | compiler_fail_on_warning: false, 4 | compiler_hash_type: 'chunkhash', 5 | compiler_devtool: null, 6 | compiler_stats: { 7 | chunks: true, 8 | chunkModules: true, 9 | colors: true 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/styles/components/IndexSidebar.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.index-sidebar__logo': { 4 | margin: 0, 5 | fontSize: '224px', 6 | lineHeight: '176px', 7 | color: variables.colors.logo, 8 | userSelect: 'none' 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | 3 | const config = require('../config') 4 | const server = require('../server/main') 5 | const debug = require('debug')('app:bin:server') 6 | 7 | const port = config.server_port 8 | 9 | server.listen(port) 10 | debug('Server is now running at localhost:' + port + '.') 11 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import connect from 'gulp-connect' 2 | import ghPages from 'gulp-gh-pages' 3 | import gulp from 'gulp' 4 | 5 | gulp.task('deploy', () => 6 | gulp.src('./dist/**/*') 7 | .pipe(ghPages()) 8 | ) 9 | 10 | gulp.task('serve-dist', () => 11 | connect.server({ 12 | root: 'dist', 13 | port: 8001, 14 | livereload: false 15 | }) 16 | ) 17 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, IndexRoute } from 'react-router' 3 | import App from 'containers/App' 4 | import Index from 'containers/Index' 5 | import IndexSidebar from 'containers/IndexSidebar' 6 | 7 | export default ( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/styles/index.js: -------------------------------------------------------------------------------- 1 | import R from 'ramda' 2 | import globalStyles from './globalStyles' 3 | import components from './components/all' 4 | import _variables from './variables' 5 | 6 | export default(themeName) => { 7 | const variables = _variables(themeName) 8 | return R.reduce( 9 | (styles, component) => R.merge(styles, component(variables)), 10 | {}, 11 | R.append(globalStyles, components) 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routeReducer } from 'redux-simple-router' 3 | import calculations from './modules/calculations' 4 | import keys from './modules/keys' 5 | import settings from './modules/settings' 6 | import themes from './modules/themes' 7 | 8 | export default combineReducers({ 9 | calculations, 10 | keys, 11 | router: routeReducer, 12 | settings, 13 | themes 14 | }) 15 | -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createDevTools } from 'redux-devtools' 3 | import LogMonitor from 'redux-devtools-log-monitor' 4 | import DockMonitor from 'redux-devtools-dock-monitor' 5 | 6 | export default createDevTools( 7 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import _debug from 'debug' 2 | 3 | const debug = _debug('app:config') 4 | debug('Create configuration.') 5 | import base from './_base' 6 | 7 | debug(`Apply environment overrides for NODE_ENV "${base.env}".`) 8 | let overrides 9 | try { 10 | overrides = require(`./_${base.env}`)(base) 11 | } catch (error) { 12 | debug( 13 | `No configuration overrides found for NODE_ENV "${base.env}"` 14 | ) 15 | } 16 | 17 | export default Object.assign({}, base, overrides) 18 | -------------------------------------------------------------------------------- /server/lib/apply-express-middleware.js: -------------------------------------------------------------------------------- 1 | // Based on: 2 | // https://github.com/dayAlone/koa-webpack-hot-middleware/blob/master/index.js 3 | export default function applyExpressMiddleware(fn, req, res) { 4 | const originalEnd = res.end 5 | 6 | return new Promise((resolve, reject) => { 7 | res.end = function () { 8 | originalEnd.apply(this, arguments) 9 | resolve(false) 10 | } 11 | fn(req, res, function () { 12 | resolve(true) 13 | }) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/layout/base.js: -------------------------------------------------------------------------------- 1 | export default { 2 | gutters: { 3 | xlarge: 50, 4 | large: 40, 5 | base: 20, 6 | small: 10, 7 | xsmall: 6, 8 | tiny: 3 9 | }, 10 | screens: { 11 | mediumWidth: '(max-width: 992px) and (min-width: 646px)', 12 | smallWidth: '@media (max-width: 645px)', 13 | mediumHeight: '(max-height: 800px) and (min-height: 446px)', 14 | smallHeight: '(max-height: 445px)' 15 | }, 16 | layout: { 17 | sidebarWidth: '270px' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/variables.js: -------------------------------------------------------------------------------- 1 | import R from 'ramda' 2 | import baseThemeVariables from './themes/base' 3 | import baseLayoutVariables from './layout/base' 4 | import createTheme from './themes/createTheme' 5 | 6 | export default function (themeName) { 7 | const themeVariables = themeName 8 | ? createTheme(require('styles/themes/' + themeName).default) 9 | : {} 10 | 11 | return R.mergeAll([ 12 | baseLayoutVariables, 13 | baseThemeVariables, 14 | themeVariables 15 | ]) 16 | } 17 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | - javascript 9 | - python 10 | - php 11 | eslint: 12 | enabled: true 13 | fixme: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.inc" 18 | - "**.js" 19 | - "**.jsx" 20 | - "**.module" 21 | - "**.php" 22 | - "**.py" 23 | - "**.rb" 24 | exclude_paths: 25 | - bin/ 26 | - build/ 27 | - config/ 28 | - server/ 29 | - tests/ 30 | -------------------------------------------------------------------------------- /src/initialState.js: -------------------------------------------------------------------------------- 1 | import basicArithmetic from 'keyboardLayouts/basicArithmetic' 2 | 3 | export default { 4 | keys: basicArithmetic, 5 | settings: { 6 | authorName: 'Panagiotis Panagi', 7 | authorUrl: 'https://github.com/panayi', 8 | repoUrl: 'https://github.com/panayi/calculator', 9 | tweetText: '3R Calculator built with React, Redux and Ramda', 10 | tweetVia: 'ppanagi' 11 | }, 12 | themes: [ 13 | { name: 'dark', active: true }, 14 | { name: 'light', active: false } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tests/test-helpers/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-addons-test-utils' 3 | 4 | function _shallowRender(component) { 5 | const renderer = TestUtils.createRenderer() 6 | renderer.render(component) 7 | return renderer.getRenderOutput() 8 | } 9 | 10 | export function render(Component, props = {}) { 11 | return TestUtils.renderIntoDocument() 12 | } 13 | 14 | export function shallowRender(Component, props = {}) { 15 | return _shallowRender() 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/components/Calculate.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.calculate': { 4 | position: 'relative' 5 | }, 6 | '.calculate__input': { 7 | background: 'none', 8 | border: 'none', 9 | boxShadow: 'none', 10 | outline: 'none', 11 | fontSize: '20px', 12 | padding: '11px 0 17px 0', 13 | width: '100%', 14 | color: variables.colors.accent 15 | }, 16 | '.calculate__output': { 17 | position: 'absolute', 18 | bottom: '2px', 19 | fontSize: variables.fontSizes.small 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/redux/modules/events.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | 3 | // ------------------------------------ 4 | // Constants 5 | // ------------------------------------ 6 | export const actionTypes = { 7 | KEY_CLICKED: 'KEY_CLICKED', 8 | KEY_PRESSED: 'KEY_PRESSED' 9 | } 10 | 11 | // ------------------------------------ 12 | // Actions 13 | // ------------------------------------ 14 | 15 | // keyClicked :: Key -> Action 16 | export const keyClicked = createAction(actionTypes.KEY_CLICKED) 17 | 18 | // keyPressed :: Event -> Action 19 | export const keyPressed = createAction(actionTypes.KEY_PRESSED) 20 | -------------------------------------------------------------------------------- /tests/framework.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | 3 | describe('(Framework) Karma Plugins', function () { 4 | it('Should expose "expect" globally.', function () { 5 | assert.ok(expect) 6 | }) 7 | 8 | it('Should expose "should" globally.', function () { 9 | assert.ok(should) 10 | }) 11 | 12 | it('Should have chai-as-promised helpers.', function () { 13 | const pass = new Promise(res => res('test')) 14 | const fail = new Promise((res, rej) => rej()) 15 | 16 | return Promise.all([ 17 | expect(pass).to.be.fulfilled, 18 | expect(fail).to.not.be.fulfilled 19 | ]) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/styles/components/all.js: -------------------------------------------------------------------------------- 1 | import mediaQueries from './_mediaQueries' 2 | import App from './App' 3 | import Author from './Author' 4 | import Button from './Button' 5 | import Calculate from './Calculate' 6 | import Calculation from './Calculation' 7 | import CalculationsList from './CalculationsList' 8 | import Index from './Index' 9 | import IndexSidebar from './IndexSidebar' 10 | import ThemeSelector from './ThemeSelector' 11 | 12 | export default [ 13 | mediaQueries, 14 | App, 15 | Author, 16 | Button, 17 | Calculate, 18 | Calculation, 19 | CalculationsList, 20 | Index, 21 | IndexSidebar, 22 | ThemeSelector 23 | ] 24 | -------------------------------------------------------------------------------- /server/middleware/webpack-hmr.js: -------------------------------------------------------------------------------- 1 | import WebpackHotMiddleware from 'webpack-hot-middleware' 2 | import applyExpressMiddleware from '../lib/apply-express-middleware' 3 | import _debug from 'debug' 4 | 5 | const debug = _debug('app:server:webpack-hmr') 6 | 7 | export default function (compiler, opts) { 8 | debug('Enable Webpack Hot Module Replacement (HMR).') 9 | 10 | const middleware = WebpackHotMiddleware(compiler, opts) 11 | return async function koaWebpackHMR(ctx, next) { 12 | let hasNext = await applyExpressMiddleware(middleware, ctx.req, ctx.res) // eslint-disable-line prefer-const, max-len 13 | 14 | if (hasNext && next) { 15 | await next() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /src/styles/components/Button.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.button': { 4 | fontSize: variables.fontSizes.button, 5 | width: '56.5px', 6 | margin: `0 ${variables.gutters.xsmall}px ${variables.gutters.xsmall}px 0`, 7 | padding: '1px 0', 8 | textAlign: 'center', 9 | display: 'inline-block', 10 | cursor: 'pointer', 11 | userSelect: 'none', 12 | borderBottom: `5px solid ${variables.colors.canvasDarker}`, 13 | backgroundColor: variables.colors.canvasDark 14 | }, 15 | '.button--active': { 16 | color: variables.colors.accent, 17 | backgroundColor: variables.colors.canvasDarker 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/redux/modules/events.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | import { keyPressed, actionTypes } from 'redux/modules/events' 3 | 4 | describe('(Redux Module) events', function () { 5 | let event 6 | 7 | describe('actions', function () { 8 | beforeEach(function () { 9 | event = $.Event('keypress', { 10 | which: 100, 11 | keyCode: 100 12 | }) 13 | }) 14 | 15 | it('should create an action for handling keyPress event', function () { 16 | const expectedAction = { 17 | type: actionTypes.KEY_PRESSED, 18 | payload: event 19 | } 20 | expect(keyPressed(event)).to.deep.equal(expectedAction) 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/test-helpers/shouldComponentUpdate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | import R from 'ramda' 3 | 4 | const assert = function (result) { 5 | return { 6 | is: { 7 | true() { 8 | expect(result).to.be.true 9 | }, 10 | 11 | false() { 12 | expect(result).to.be.false 13 | } 14 | } 15 | } 16 | } 17 | 18 | export function shouldUpdate(component, changedProps) { 19 | const nextProps = R.merge(component.props, changedProps) 20 | return assert(component.shouldComponentUpdate(nextProps)) 21 | } 22 | 23 | export function shouldIgnoreOtherProps(component, nextProps) { 24 | expect(component.shouldComponentUpdate(nextProps)).to.be.false 25 | } 26 | -------------------------------------------------------------------------------- /tests/test-bundler.js: -------------------------------------------------------------------------------- 1 | // require all `tests/test-helpers/**/*.js` 2 | const helpersContext = require.context('./test-helpers/', true, /\.js$/) 3 | helpersContext.keys().forEach(helpersContext) 4 | 5 | // require all `tests/**/*.spec.js` 6 | const testsContext = require.context('./', true, /\.spec\.js$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all `src/**/*.js` except for `main.js` (for isparta reporting) 10 | const componentsContext = require.context('../src/', true, /^((?!main).)*\.js$/) 11 | 12 | componentsContext.keys().forEach(componentsContext) 13 | 14 | // global.navigator = {userAgent: 'Mozilla/5.0 (Windows NT 6.1; WOW64) 15 | // AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2454.85 Safari/537.36'}; 16 | -------------------------------------------------------------------------------- /src/redux/modules/settings.js: -------------------------------------------------------------------------------- 1 | import { handleActions, createAction } from 'redux-actions' 2 | import R from 'ramda' 3 | 4 | // ------------------------------------ 5 | // Constants 6 | // ------------------------------------ 7 | export const actionTypes = { 8 | SET_SETTING: 'SET_SETTING' 9 | } 10 | 11 | // ------------------------------------ 12 | // Actions 13 | // ------------------------------------ 14 | 15 | // setSetting :: Object -> Action 16 | export const setSetting = createAction(actionTypes.SET_SETTING) 17 | 18 | // ------------------------------------ 19 | // Reducer 20 | // ------------------------------------ 21 | export default handleActions({ 22 | [actionTypes.SET_SETTING]: (state, { payload }) => R.merge(state, payload) 23 | }, {}) 24 | -------------------------------------------------------------------------------- /tests/redux/modules/settings.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | import R from 'ramda' 3 | import reducer, { actionTypes } from 'redux/modules/settings' 4 | 5 | describe('(Redux Module) settings', function () { 6 | describe('reducer', function () { 7 | it('should return the initial state', function () { 8 | expect(reducer(undefined, {})).to.deep.equal({}) 9 | }) 10 | 11 | it('should handle SET_SETTING', function () { 12 | const initialState = { initialKey: 'foo' } 13 | const setting = { bar: 'baz' } 14 | 15 | expect(reducer(initialState, { 16 | type: actionTypes.SET_SETTING, 17 | payload: setting 18 | })).to.deep.equal(R.merge(initialState, setting)) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/keyboardLayouts/basicArithmetic.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { keyCode: 40, display: '(' }, 3 | { keyCode: 41, display: ')' }, 4 | { keyCode: 94, display: '^' }, 5 | { keyCode: 55, display: '7' }, 6 | { keyCode: 56, display: '8' }, 7 | { keyCode: 57, display: '9' }, 8 | { keyCode: 47, display: '÷' }, 9 | { keyCode: 52, display: '4' }, 10 | { keyCode: 53, display: '5' }, 11 | { keyCode: 54, display: '6' }, 12 | { keyCode: 42, display: '×' }, 13 | { keyCode: 49, display: '1' }, 14 | { keyCode: 50, display: '2' }, 15 | { keyCode: 51, display: '3' }, 16 | { keyCode: 45, display: '−' }, 17 | { keyCode: 48, display: '0' }, 18 | { keyCode: 46, display: '.' }, 19 | { keyCode: 13, display: '=' }, 20 | { keyCode: 43, display: '+' } 21 | ] 22 | -------------------------------------------------------------------------------- /src/styles/components/Calculation.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.calculation__output': { 4 | fontSize: variables.fontSizes.xlarge, 5 | color: variables.colors.accent 6 | }, 7 | '.calculation__input': { 8 | fontSize: variables.fontSizes.small, 9 | marginTop: - variables.gutters.tiny 10 | }, 11 | '.calculation__pointer': { 12 | textAlign: 'right', 13 | width: `${variables.gutters.xlarge}px`, 14 | cursor: 'pointer', 15 | color: variables.colors.fadedText, 16 | userSelect: 'none' 17 | }, 18 | '.calculation__pointer span': { 19 | padding: `${variables.gutters.tiny}px` 20 | }, 21 | '.calculation__pointer:hover': { 22 | color: variables.colors.text 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/components/CalculationsList.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | '.calculations-list__content': { 4 | overflow: 'auto', 5 | marginTop: `${- variables.gutters.small}px` 6 | }, 7 | '.calculations-list__overlay': { 8 | position: 'absolute', 9 | zIndex: 0, 10 | top: 0, 11 | bottom: 0, 12 | left: 0, 13 | right: 0, 14 | display: 'table-cell', 15 | width: '50%', 16 | height: '7.5vw', 17 | margin: 'auto', 18 | paddingLeft: `${variables.gutters.xlarge}px`, 19 | fontSize: '6vw', 20 | textAlign: 'center', 21 | color: variables.colors.fadedText, 22 | userSelect: 'none' 23 | }, 24 | '.calculations-list__3r': { 25 | display: 'none', 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/themes/base.js: -------------------------------------------------------------------------------- 1 | export default { 2 | colors: { 3 | 4 | }, 5 | fontFamilies: { 6 | text: '\'Lato\', \'Helvetica\', \'Arial\', sans-serif', 7 | header: '\'Lobster\', sans-serif' 8 | }, 9 | fontSizes: { 10 | base: '14px', 11 | large: '18px', 12 | small: '12px', 13 | xlarge: '22px', 14 | button: '37px' 15 | }, 16 | gutters: { 17 | xlarge: 50, 18 | large: 40, 19 | base: 20, 20 | small: 10, 21 | xsmall: 6, 22 | tiny: 3 23 | }, 24 | layout: { 25 | sidebarWidth: '270px' 26 | }, 27 | screens: { 28 | mediumWidth: '(max-width: 992px) and (min-width: 646px)', 29 | smallWidth: '(max-width: 645px)', 30 | mediumHeight: '(max-height: 800px) and (min-height: 446px)', 31 | smallHeight: '(max-height: 445px)' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/globalStyles.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | body: { 4 | fontFamily: variables.fontFamilies.text, 5 | fontSize: variables.fontSizes.base, 6 | backgroundColor: variables.colors.canvas, 7 | color: variables.colors.text 8 | }, 9 | 10 | 'h1, h2': { 11 | fontFamily: variables.fontFamilies.header, 12 | fontWeight: 'normal' 13 | }, 14 | 15 | h1: { 16 | fontSize: variables.fontSizes.xlarge 17 | }, 18 | 19 | h2: { 20 | fontSize: variables.fontSizes.large 21 | }, 22 | 23 | a: { 24 | color: variables.colors.text, 25 | textDecoration: 'none' 26 | }, 27 | 28 | 'a:hover': { 29 | color: variables.colors.accent 30 | }, 31 | 32 | small: { 33 | fontSize: variables.fontSizes.small 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { propsChanged } from 'helpers/pureFunctions' 3 | import Flex from 'components/Flex' 4 | 5 | export default class App extends Component { 6 | static propTypes = { 7 | main: PropTypes.element.isRequired, 8 | sidebar: PropTypes.element, 9 | }; 10 | 11 | shouldComponentUpdate(nextProps) { 12 | return propsChanged(['main', 'sidebar'], this.props, nextProps) 13 | } 14 | 15 | render() { 16 | const { main, sidebar } = this.props 17 | 18 | return ( 19 | 20 | 21 | {sidebar} 22 | 23 | 24 | {main} 25 | 26 | 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3R Calculator | React, Redux, Ramda 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/redux/modules/themes.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | import R from 'ramda' 3 | import reducer, { actionTypes } from 'redux/modules/themes' 4 | 5 | describe('(Redux Module) theme', function () { 6 | describe('reducer', function () { 7 | it('should return the initial state', function () { 8 | expect(reducer(undefined, {})).to.deep.equal([]) 9 | }) 10 | 11 | it('should handle SET_THEME', function () { 12 | const theme1 = { name: 'foo', active: false } 13 | const theme2 = { name: 'bar', active: false } 14 | const initialState = [R.merge(theme1, { active: true }), theme2] 15 | 16 | expect(reducer(initialState, { 17 | type: actionTypes.ACTIVATE_THEME, 18 | payload: theme2.name 19 | })).to.deep.equal([theme1, R.merge(theme2, { active: true })]) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createHistory, useBasename } from 'history' 4 | import { syncReduxAndRouter } from 'redux-simple-router' 5 | import routes from './routes' 6 | import Root from './containers/Root' 7 | import configureStore from './redux/configureStore' 8 | import initialState from 'initialState' 9 | 10 | const history = useBasename(createHistory)({ 11 | basename: __BASENAME__ 12 | }) 13 | const store = configureStore(initialState) 14 | 15 | syncReduxAndRouter(history, store, (state) => state.router) 16 | 17 | // Render the React application to the DOM 18 | ReactDOM.render( 19 | , 20 | document.getElementById('root') 21 | ) 22 | 23 | // Log current tag-commit 24 | console.log(`%c 3R Calculator: ${GIT.tag}@${GIT.commit}`, 'color: #2CA127') 25 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import classnames from 'classnames' 3 | import { propsChanged } from 'helpers/pureFunctions' 4 | 5 | export default class Button extends Component { 6 | static propTypes = { 7 | active: PropTypes.bool, 8 | children: PropTypes.node, 9 | onClick: PropTypes.func, 10 | }; 11 | 12 | shouldComponentUpdate(nextProps) { 13 | return propsChanged(['active', 'children'], this.props, nextProps) 14 | } 15 | 16 | render() { 17 | const { 18 | active, 19 | children, 20 | onClick, 21 | } = this.props 22 | 23 | const btnClass = classnames({ 24 | button: true, 25 | 'button--active': active, 26 | }) 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/components/_mediaQueries.js: -------------------------------------------------------------------------------- 1 | export default function (variables) { 2 | return { 3 | mediaQueries: { 4 | [variables.screens.mediumWidth]: { 5 | '.author__name': { 6 | display: 'none !important' 7 | } 8 | }, 9 | 10 | [variables.screens.smallWidth]: { 11 | '.app__sidebar': { 12 | display: 'none !important' 13 | }, 14 | '.calculations-list__3r': { 15 | display: 'inline !important' 16 | } 17 | }, 18 | 19 | [variables.screens.mediumHeight]: { 20 | '.index-sidebar__logo': { 21 | fontSize: '28vh !important', 22 | lineHeight: '22vh !important' 23 | } 24 | }, 25 | 26 | [variables.screens.smallHeight]: { 27 | '.index-sidebar__logo': { 28 | display: 'none !important' 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/redux/modules/themes.js: -------------------------------------------------------------------------------- 1 | import { handleActions, createAction } from 'redux-actions' 2 | import R from 'ramda' 3 | 4 | // ------------------------------------ 5 | // Constants 6 | // ------------------------------------ 7 | export const actionTypes = { 8 | ACTIVATE_THEME: 'ACTIVATE_THEME' 9 | } 10 | 11 | // ------------------------------------ 12 | // Actions 13 | // ------------------------------------ 14 | 15 | // activateTheme :: String -> Action 16 | export const activateTheme = createAction(actionTypes.ACTIVATE_THEME) 17 | 18 | // ------------------------------------ 19 | // Reducer 20 | // ------------------------------------ 21 | export default handleActions({ 22 | [actionTypes.ACTIVATE_THEME]: (state, { payload }) => R.map( 23 | R.converge(R.set(R.lensProp('active')), [ 24 | R.compose(R.equals(payload), R.prop('name')), 25 | R.identity 26 | ]), 27 | state 28 | ) 29 | }, []) 30 | -------------------------------------------------------------------------------- /src/containers/ThemeManager.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { Style } from 'radium' 4 | import { createStructuredSelector } from 'reselect' 5 | import { propsChanged } from 'helpers/pureFunctions' 6 | import { activeThemeNameSelector } from 'redux/selectors' 7 | import createStyles from 'styles' 8 | import 'normalize.css/normalize.css' 9 | 10 | export class ThemeManager extends Component { 11 | static propTypes = { 12 | activeThemeName: PropTypes.string.isRequired, 13 | }; 14 | 15 | shouldComponentUpdate(nextProps) { 16 | return propsChanged(['activeThemeName'], this.props, nextProps) 17 | } 18 | 19 | render() { 20 | const { activeThemeName } = this.props 21 | const styles = createStyles(activeThemeName) 22 | 23 | return ( 24 |