├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── files ├── browserstack.png ├── browserstack.svg └── grabients.sketch ├── flow-typed ├── npm │ ├── autoprefixer_vx.x.x.js │ ├── babel-core_vx.x.x.js │ ├── babel-eslint_vx.x.x.js │ ├── babel-jest_vx.x.x.js │ ├── babel-loader_vx.x.x.js │ ├── babel-preset-react-app_vx.x.x.js │ ├── babel-runtime_vx.x.x.js │ ├── case-sensitive-paths-webpack-plugin_vx.x.x.js │ ├── chalk_v1.x.x.js │ ├── css-loader_vx.x.x.js │ ├── deep-equal_vx.x.x.js │ ├── dotenv_vx.x.x.js │ ├── eslint-config-airbnb_vx.x.x.js │ ├── eslint-config-prettier_vx.x.x.js │ ├── eslint-config-react-app_vx.x.x.js │ ├── eslint-config-react_vx.x.x.js │ ├── eslint-loader_vx.x.x.js │ ├── eslint-plugin-flow_vx.x.x.js │ ├── eslint-plugin-flowtype_vx.x.x.js │ ├── eslint-plugin-import_vx.x.x.js │ ├── eslint-plugin-jsx-a11y_vx.x.x.js │ ├── eslint-plugin-prettier_vx.x.x.js │ ├── eslint-plugin-react_vx.x.x.js │ ├── eslint_vx.x.x.js │ ├── extract-text-webpack-plugin_vx.x.x.js │ ├── file-loader_vx.x.x.js │ ├── flow-bin_v0.x.x.js │ ├── fs-extra_vx.x.x.js │ ├── html-webpack-plugin_vx.x.x.js │ ├── jest_v20.x.x.js │ ├── lodash_v4.x.x.js │ ├── object-assign_v4.x.x.js │ ├── postcss-flexbugs-fixes_vx.x.x.js │ ├── postcss-loader_vx.x.x.js │ ├── prettier_vx.x.x.js │ ├── promise_vx.x.x.js │ ├── react-color_vx.x.x.js │ ├── react-dev-utils_vx.x.x.js │ ├── react-error-overlay_vx.x.x.js │ ├── react-icons_vx.x.x.js │ ├── react-move_vx.x.x.js │ ├── react-redux_v5.x.x.js │ ├── react-share_vx.x.x.js │ ├── react-sortable-hoc_vx.x.x.js │ ├── redux-logger_vx.x.x.js │ ├── redux-thunk_vx.x.x.js │ ├── redux_v3.x.x.js │ ├── reselect_v3.x.x.js │ ├── standard_vx.x.x.js │ ├── style-loader_vx.x.x.js │ ├── styled-components_v2.x.x.js │ ├── sw-precache-webpack-plugin_vx.x.x.js │ ├── url-loader_vx.x.x.js │ ├── webpack-dev-server_vx.x.x.js │ ├── webpack-manifest-plugin_vx.x.x.js │ ├── webpack_vx.x.x.js │ └── whatwg-fetch_vx.x.x.js └── types.js ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── grabber.png ├── index.html ├── manifest.json ├── mstile-150x150.png └── safari-pinned-tab.svg ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── App.js ├── assets │ ├── eddie.png │ ├── grabients.sketch │ ├── john.png │ └── rora-grabient@2x.png ├── components │ ├── ActionGroup │ │ ├── Container.js │ │ └── Item.js │ ├── AddDeleteStop │ │ └── AddDeleteStop.js │ ├── AnglePreview │ │ └── AnglePreview.js │ ├── Common │ │ ├── Button.js │ │ ├── Checkbox.js │ │ ├── DashedBar.js │ │ ├── Logo.js │ │ ├── Triangle.js │ │ ├── Typography.js │ │ ├── UnfoldLogo.js │ │ └── index.js │ ├── Footer │ │ └── Footer.js │ ├── Gradient │ │ └── Gradient.js │ ├── GradientContainer │ │ └── GradientContainer.js │ ├── GradientList │ │ └── GradientList.js │ ├── Icons │ │ ├── AnglePrev.js │ │ ├── Arrow.js │ │ ├── Check.js │ │ ├── Close.js │ │ ├── Copy.js │ │ ├── Edit.js │ │ ├── ExpandEdit.js │ │ ├── PaginationArrow.js │ │ ├── Reset.js │ │ ├── Sketch.js │ │ ├── Trash.js │ │ └── index.js │ ├── Popover │ │ └── Popover.js │ ├── Sections │ │ ├── GradientDisplay.js │ │ └── Hero.js │ ├── Swatch │ │ ├── Container.js │ │ ├── Item.js │ │ └── index.js │ └── index.js ├── containers │ ├── ActionsGroup │ │ └── ActionsGroup.js │ ├── AngleWheel │ │ └── AngleWheel.js │ ├── ColorPicker │ │ └── ColorPicker.js │ ├── GradientCard │ │ └── GradientCard.js │ ├── Pagination │ │ ├── Item.js │ │ └── Pagination.js │ ├── SortableSwatch │ │ └── SortableSwatch.js │ └── index.js ├── index.css ├── index.js ├── store │ ├── dimensions │ │ ├── actions.js │ │ ├── reducer.js │ │ └── selector.js │ ├── gradients │ │ ├── actions.js │ │ ├── reducer.js │ │ └── selectors.js │ ├── icons │ │ ├── actions.js │ │ ├── reducer.js │ │ └── utils.js │ ├── reducers.js │ ├── settings │ │ ├── actions.js │ │ └── reducer.js │ ├── stops │ │ ├── actions.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ └── utils.js │ └── store.js ├── utils │ ├── gradient.js │ ├── gradient.test.js │ └── localStorage.js └── wheel.png └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/ 2 | node_modules/ 3 | build/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:flowtype/recommended", "airbnb", "prettier", "prettier/react"], 3 | "rules": { 4 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], 5 | "no-shadow": 0, 6 | "jsx-a11y/no-autofocus": 0, 7 | "consistent-return": 0, 8 | "react/no-did-mount-set-state": 0, 9 | "no-plusplus": 0, 10 | "react/prop-types": 1 11 | }, 12 | "plugins": ["flowtype", "prettier"], 13 | "parser": "babel-eslint", 14 | "parserOptions": { 15 | "ecmaVersion": 2017, 16 | "sourceType": "module", 17 | "ecmaFeatures": { 18 | "jsx": true 19 | } 20 | }, 21 | "env": { 22 | "es6": true, 23 | "browser": true, 24 | "node": true, 25 | "jest": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/styled-components/* 3 | 4 | [include] 5 | 6 | [libs] 7 | styled-components 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /files/source.sketch 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "standard.enable": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grabient 2 | 3 | UI to generate linear web gradient 4 | 5 | ## To dos 6 | - [ ] add prop validation with flow 7 | 8 | ## Installation 9 | 1. Clone the repo 10 | 2. run `npm install or yarn install` 11 | 3. run `npm run start or yarn start` for a development build `npm run build or yarn build` for a production build 12 | 13 | #### Special thanks to BrowserStack for providing the very much needed browser testing 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce( 82 | (env, key) => { 83 | env[key] = JSON.stringify(raw[key]); 84 | return env; 85 | }, 86 | {} 87 | ), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Grabient", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/android-chrome-256x256.png", 11 | "sizes": "256x256", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone" 18 | } 19 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkorzhuk/grabient/ca0c737d478a86ebe22d3c1b5a34256b68d5b711/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.NODE_ENV = 'production'; 5 | 6 | // Makes the script crash on unhandled rejections instead of silently 7 | // ignoring them. In the future, promise rejections that are not handled will 8 | // terminate the Node.js process with a non-zero exit code. 9 | process.on('unhandledRejection', err => { 10 | throw err; 11 | }); 12 | 13 | // Ensure environment variables are read. 14 | require('../config/env'); 15 | 16 | const path = require('path'); 17 | const chalk = require('chalk'); 18 | const fs = require('fs-extra'); 19 | const webpack = require('webpack'); 20 | const config = require('../config/webpack.config.prod'); 21 | const paths = require('../config/paths'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 24 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 25 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 26 | 27 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 28 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 29 | const useYarn = fs.existsSync(paths.yarnLockFile); 30 | 31 | // Warn and crash if required files are missing 32 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 33 | process.exit(1); 34 | } 35 | 36 | // First, read the current file sizes in build directory. 37 | // This lets us display how much they changed later. 38 | measureFileSizesBeforeBuild(paths.appBuild) 39 | .then(previousFileSizes => { 40 | // Remove all content but keep the directory so that 41 | // if you're in it, you don't end up in Trash 42 | fs.emptyDirSync(paths.appBuild); 43 | // Merge with the public folder 44 | copyPublicFolder(); 45 | // Start the webpack build 46 | return build(previousFileSizes); 47 | }) 48 | .then( 49 | ({ stats, previousFileSizes, warnings }) => { 50 | if (warnings.length) { 51 | console.log(chalk.yellow('Compiled with warnings.\n')); 52 | console.log(warnings.join('\n\n')); 53 | console.log( 54 | '\nSearch for the ' + 55 | chalk.underline(chalk.yellow('keywords')) + 56 | ' to learn more about each warning.' 57 | ); 58 | console.log( 59 | 'To ignore, add ' + 60 | chalk.cyan('// eslint-disable-next-line') + 61 | ' to the line before.\n' 62 | ); 63 | } else { 64 | console.log(chalk.green('Compiled successfully.\n')); 65 | } 66 | 67 | console.log('File sizes after gzip:\n'); 68 | printFileSizesAfterBuild(stats, previousFileSizes); 69 | console.log(); 70 | 71 | const appPackage = require(paths.appPackageJson); 72 | const publicUrl = paths.publicUrl; 73 | const publicPath = config.output.publicPath; 74 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 75 | printHostingInstructions( 76 | appPackage, 77 | publicUrl, 78 | publicPath, 79 | buildFolder, 80 | useYarn 81 | ); 82 | }, 83 | err => { 84 | console.log(chalk.red('Failed to compile.\n')); 85 | console.log((err.message || err) + '\n'); 86 | process.exit(1); 87 | } 88 | ); 89 | 90 | // Create the production build and print the deployment instructions. 91 | function build(previousFileSizes) { 92 | console.log('Creating an optimized production build...'); 93 | 94 | let compiler = webpack(config); 95 | return new Promise((resolve, reject) => { 96 | compiler.run((err, stats) => { 97 | if (err) { 98 | return reject(err); 99 | } 100 | const messages = formatWebpackMessages(stats.toJson({}, true)); 101 | if (messages.errors.length) { 102 | return reject(new Error(messages.errors.join('\n\n'))); 103 | } 104 | if (process.env.CI && messages.warnings.length) { 105 | console.log( 106 | chalk.yellow( 107 | '\nTreating warnings as errors because process.env.CI = true.\n' + 108 | 'Most CI servers set it automatically.\n' 109 | ) 110 | ); 111 | return reject(new Error(messages.warnings.join('\n\n'))); 112 | } 113 | return resolve({ 114 | stats, 115 | previousFileSizes, 116 | warnings: messages.warnings, 117 | }); 118 | }); 119 | }); 120 | } 121 | 122 | function copyPublicFolder() { 123 | fs.copySync(paths.appPublic, paths.appBuild, { 124 | dereference: true, 125 | filter: file => file !== paths.appHtml, 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Makes the script crash on unhandled rejections instead of silently 4 | // ignoring them. In the future, promise rejections that are not handled will 5 | // terminate the Node.js process with a non-zero exit code. 6 | process.on('unhandledRejection', err => { 7 | throw err; 8 | }); 9 | 10 | process.env.NODE_ENV = 'development'; 11 | 12 | // Ensure environment variables are read. 13 | require('../config/env'); 14 | 15 | const fs = require('fs'); 16 | const chalk = require('chalk'); 17 | const webpack = require('webpack'); 18 | const WebpackDevServer = require('webpack-dev-server'); 19 | const clearConsole = require('react-dev-utils/clearConsole'); 20 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 21 | const { 22 | choosePort, 23 | createCompiler, 24 | prepareProxy, 25 | prepareUrls, 26 | } = require('react-dev-utils/WebpackDevServerUtils'); 27 | const openBrowser = require('react-dev-utils/openBrowser'); 28 | const paths = require('../config/paths'); 29 | const config = require('../config/webpack.config.dev'); 30 | const createDevServerConfig = require('../config/webpackDevServer.config'); 31 | 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | const isInteractive = process.stdout.isTTY; 34 | 35 | // Warn and crash if required files are missing 36 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 37 | process.exit(1); 38 | } 39 | 40 | // Tools like Cloud9 rely on this. 41 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 42 | const HOST = process.env.HOST || '0.0.0.0'; 43 | 44 | // We attempt to use the default port but if it is busy, we offer the user to 45 | // run on a different port. `detect()` Promise resolves to the next free port. 46 | choosePort(HOST, DEFAULT_PORT) 47 | .then(port => { 48 | if (port == null) { 49 | // We have not found a port. 50 | return; 51 | } 52 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 53 | const appName = require(paths.appPackageJson).name; 54 | const urls = prepareUrls(protocol, HOST, port); 55 | // Create a webpack compiler that is configured with custom messages. 56 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 57 | // Load proxy config 58 | const proxySetting = require(paths.appPackageJson).proxy; 59 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 60 | // Serve webpack assets generated by the compiler over a web sever. 61 | const serverConfig = createDevServerConfig( 62 | proxyConfig, 63 | urls.lanUrlForConfig 64 | ); 65 | const devServer = new WebpackDevServer(compiler, serverConfig); 66 | // Launch WebpackDevServer. 67 | devServer.listen(port, HOST, err => { 68 | if (err) { 69 | return console.log(err); 70 | } 71 | if (isInteractive) { 72 | clearConsole(); 73 | } 74 | console.log(chalk.cyan('Starting the development server...\n')); 75 | openBrowser(urls.localUrlForBrowser); 76 | }); 77 | 78 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 79 | process.on(sig, function() { 80 | devServer.close(); 81 | process.exit(); 82 | }); 83 | }); 84 | }) 85 | .catch(err => { 86 | if (err && err.message) { 87 | console.log(err.message); 88 | } 89 | process.exit(1); 90 | }); 91 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'test'; 4 | process.env.PUBLIC_URL = ''; 5 | 6 | // Makes the script crash on unhandled rejections instead of silently 7 | // ignoring them. In the future, promise rejections that are not handled will 8 | // terminate the Node.js process with a non-zero exit code. 9 | process.on('unhandledRejection', err => { 10 | throw err; 11 | }); 12 | 13 | // Ensure environment variables are read. 14 | require('../config/env'); 15 | 16 | const jest = require('jest'); 17 | const argv = process.argv.slice(2); 18 | 19 | // Watch unless on CI or in coverage mode 20 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 21 | argv.push('--watch'); 22 | } 23 | 24 | 25 | jest.run(argv); 26 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import React, { Component } from "react"; 3 | import { connect } from "react-redux"; 4 | import styled from "styled-components"; 5 | 6 | import { 7 | editStop, 8 | updateDraggedStopPos, 9 | updateUpdatingStop, 10 | updateActiveColorPicker, 11 | updateActiveStop, 12 | deleteActiveStop, 13 | editStopColor 14 | } from "./store/stops/actions"; 15 | import { toggleEditing, updatePage } from "./store/gradients/actions"; 16 | import { toggleTrashIcon } from "./store/icons/actions"; 17 | import { getGradients } from "./store/gradients/selectors"; 18 | 19 | import { 20 | GradientDisplay, 21 | GradientList, 22 | Hero, 23 | Footer 24 | } from "./components/index"; 25 | import { ActionsGroup, Pagination } from "./containers/index"; 26 | import { DashedBar } from "./components/Common/index"; 27 | 28 | const Overlay = styled.div` 29 | position: fixed; 30 | z-index: 20; 31 | top: 0; 32 | right: 0; 33 | left: 0; 34 | bottom: 0; 35 | `; 36 | 37 | const Dashed = DashedBar.extend` 38 | margin: 0 auto; 39 | max-width: 1060px; 40 | `; 41 | 42 | function handleNoop(e) { 43 | e.stopPropagation(); 44 | e.preventDefault(); 45 | } 46 | 47 | class App extends Component { 48 | state = { 49 | items: 9 50 | }; 51 | 52 | componentDidMount() { 53 | document.addEventListener("keydown", this._handleCancelEdits); 54 | document.addEventListener("mousemove", this._handleDocumentMouseMove); 55 | document.addEventListener("mouseup", this._handleDocumentMouseUp); 56 | 57 | 58 | console.log('test') 59 | this.setState({ 60 | items: window.outerWidth <= 970 ? 6 : 9 61 | }); 62 | } 63 | 64 | componentWillUnmount() { 65 | document.removeEventListener("keydown", this._handleCancelEdits); 66 | document.removeEventListener("mousemove", this._handleDocumentMouseMove); 67 | document.removeEventListener("mouseup", this._handleDocumentMouseUp); 68 | } 69 | 70 | _handleCancelEdits = e => { 71 | const { 72 | updateActiveColorPicker, 73 | toggleEditing, 74 | editStop, 75 | updateActiveStop, 76 | deleteActiveStop, 77 | pickingColorStop, 78 | editStopColor, 79 | gradients, 80 | updatePage, 81 | currPage 82 | } = this.props; 83 | if (e.type === "click") { 84 | handleNoop(e); 85 | updateActiveStop(null); 86 | editStopColor(null); 87 | if (pickingColorStop) { 88 | updateActiveColorPicker(null); 89 | } else { 90 | toggleEditing(null); 91 | editStop(null); 92 | } 93 | } 94 | if (e.type === "keydown") { 95 | const total = Math.ceil(Object.keys(gradients).length / this.state.items); 96 | if (e.which === 39) { 97 | handleNoop(e); 98 | const newPage = currPage + 1; 99 | if (newPage <= total) { 100 | updateActiveStop(null); 101 | editStopColor(null); 102 | updateActiveColorPicker(null); 103 | toggleEditing(null); 104 | editStop(null); 105 | updatePage(newPage); 106 | } 107 | } 108 | 109 | if (e.which === 37) { 110 | handleNoop(e); 111 | const newPage = currPage - 1; 112 | if (newPage >= 1) { 113 | updateActiveStop(null); 114 | editStopColor(null); 115 | updateActiveColorPicker(null); 116 | toggleEditing(null); 117 | editStop(null); 118 | updatePage(newPage); 119 | } 120 | } 121 | 122 | if (e.which === 27) { 123 | handleNoop(e); 124 | updateActiveStop(null); 125 | editStopColor(null); 126 | if (pickingColorStop) { 127 | updateActiveColorPicker(null); 128 | } else { 129 | toggleEditing(null); 130 | editStop(null); 131 | } 132 | } 133 | if ((e.which === 46 && e.metaKey) || (e.which === 8 && e.metaKey)) { 134 | handleNoop(e); 135 | deleteActiveStop(); 136 | } 137 | } 138 | }; 139 | 140 | _handleDocumentMouseMove = e => { 141 | if (this.props.editingStop) { 142 | if (this.props.editingStop && this.props.updating) { 143 | const { x } = e; 144 | this.props.updateDraggedStopPos(x); 145 | } 146 | } 147 | }; 148 | 149 | _handleDocumentMouseUp = () => { 150 | if (this.props.editingStop) { 151 | if (this.props.editingStop && this.props.updating) { 152 | this.props.updateUpdatingStop(null); 153 | this.props.updateDraggedStopPos(null); 154 | } 155 | if (this.props.passThreshold && this.props.renderDelete !== null) { 156 | this.props.toggleTrashIcon(null); 157 | } 158 | } 159 | }; 160 | 161 | render() { 162 | const { 163 | editingAngle, 164 | editingStop, 165 | pickingColorStop, 166 | gradients: allGradients, 167 | currPage 168 | } = this.props; 169 | const { items } = this.state; 170 | const editing = editingAngle || editingStop || pickingColorStop; 171 | const start = (currPage - 1) * items; 172 | const end = start + items; 173 | const currGradients = Object.values(allGradients).slice(start, end); 174 | 175 | return ( 176 |
177 | 178 | 179 | 180 | 181 | 182 | {editing && } 183 | 184 | 185 | 186 |
187 |
188 | ); 189 | } 190 | } 191 | 192 | export default connect( 193 | state => ({ 194 | editingAngle: state.gradients.editingAngle.id !== null, 195 | editingStop: state.stops.editing !== null, 196 | updating: state.stops.updating.stop !== null, 197 | pickingColorStop: state.stops.updating.pickingColorStop !== null, 198 | gradients: getGradients(state), 199 | renderDelete: state.icons.deleteStop, 200 | passThreshold: state.stops.updating.passThreshold, 201 | currPage: state.gradients.page 202 | }), 203 | { 204 | updatePage, 205 | toggleEditing, 206 | editStop, 207 | updateDraggedStopPos, 208 | updateUpdatingStop, 209 | updateActiveColorPicker, 210 | updateActiveStop, 211 | deleteActiveStop, 212 | toggleTrashIcon, 213 | editStopColor 214 | } 215 | )(App); 216 | -------------------------------------------------------------------------------- /src/assets/eddie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkorzhuk/grabient/ca0c737d478a86ebe22d3c1b5a34256b68d5b711/src/assets/eddie.png -------------------------------------------------------------------------------- /src/assets/grabients.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkorzhuk/grabient/ca0c737d478a86ebe22d3c1b5a34256b68d5b711/src/assets/grabients.sketch -------------------------------------------------------------------------------- /src/assets/john.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkorzhuk/grabient/ca0c737d478a86ebe22d3c1b5a34256b68d5b711/src/assets/john.png -------------------------------------------------------------------------------- /src/assets/rora-grabient@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnkorzhuk/grabient/ca0c737d478a86ebe22d3c1b5a34256b68d5b711/src/assets/rora-grabient@2x.png -------------------------------------------------------------------------------- /src/components/ActionGroup/Container.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | const GroupContainer = styled.div` 6 | margin-top: 25px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | margin-right: 0; 11 | 12 | &:last-child { 13 | margin-right: 0; 14 | } 15 | 16 | @media (max-width: 620px) { 17 | ${({ orderSM }) => (orderSM ? `order: ${orderSM}` : null)}; 18 | } 19 | `; 20 | 21 | export default GroupContainer; 22 | -------------------------------------------------------------------------------- /src/components/ActionGroup/Item.js: -------------------------------------------------------------------------------- 1 | import React, { Component, cloneElement } from 'react'; 2 | import styled from 'styled-components'; 3 | import { Animate } from 'react-move'; 4 | 5 | import { TextSM } from './../Common/Typography'; 6 | 7 | const Container = styled.div` 8 | display: flex; 9 | flex-wrap: wrap; 10 | align-items: center; 11 | margin-right: 30px; 12 | cursor: pointer; 13 | 14 | &:last-child { 15 | margin-right: 0; 16 | } 17 | 18 | @media (max-width: 550px) { 19 | flex-direction: column; 20 | justify-content: center; 21 | } 22 | `; 23 | 24 | const Pretext = TextSM.extend` 25 | color: #afafaf; 26 | order: -1; 27 | 28 | @media (max-width: 550px) { 29 | margin-bottom: 5px; 30 | } 31 | `; 32 | 33 | const LinkContainer = Container.withComponent('a'); 34 | 35 | const ItemContainer = styled.div` 36 | margin-left: ${({ ml }) => `${ml}px`}; 37 | 38 | @media (max-width: 550px) { 39 | margin-left: 0; 40 | } 41 | `; 42 | 43 | class ActionGroupItem extends Component { 44 | state = { 45 | hovered: false 46 | }; 47 | 48 | handleMouseEnter = () => { 49 | this.setState({ 50 | hovered: true 51 | }); 52 | }; 53 | 54 | handleMouseLeave = () => { 55 | this.setState({ 56 | hovered: false 57 | }); 58 | }; 59 | 60 | render() { 61 | const { children, ml = 10, itemStyle, style, id, href, pretext, checked, ...props } = this.props; 62 | const { hovered } = this.state; 63 | 64 | return ( 65 | 70 | {data => { 71 | if (href) { 72 | return ( 73 | 81 | {pretext 82 | ? 83 | {pretext} 84 | 85 | : null} 86 | {cloneElement(children[0], { 87 | style: { 88 | color: data.color 89 | } 90 | })} 91 | 92 | {cloneElement(children[1], { 93 | color: data.color, 94 | id 95 | })} 96 | 97 | 98 | ); 99 | } 100 | return ( 101 | 107 | {cloneElement(children[0], { 108 | style: { 109 | color: data.color 110 | } 111 | })} 112 | 113 | {cloneElement(children[1], { 114 | color: data.color, 115 | id 116 | })} 117 | 118 | 119 | ); 120 | }} 121 | 122 | ); 123 | } 124 | } 125 | 126 | export default ActionGroupItem; 127 | -------------------------------------------------------------------------------- /src/components/AddDeleteStop/AddDeleteStop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ExpandEdit, Trash } from './../Icons/index'; 4 | 5 | const AddDeleteStop = ({ renderDelete, animationDuration, hovered, color, title }) => { 6 | if (renderDelete) { 7 | return ; 8 | } 9 | return ; 10 | }; 11 | 12 | export default AddDeleteStop; 13 | -------------------------------------------------------------------------------- /src/components/AnglePreview/AnglePreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { AnglePrev } from './../Icons/index'; 5 | import { TextMD } from './../Common/Typography'; 6 | 7 | const Container = styled.div` 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | 13 | const AngleText = TextMD.extend` 14 | padding-left: 10px; 15 | padding-top: 3px; 16 | `; 17 | 18 | const AnglePreview = ({ angle, iconAnimationDuration, hovered, editingStop, editingAngle, color }) => 19 | !editingStop && 20 | 21 | 28 | 29 | 34 | {angle}° 35 | 36 | ; 37 | 38 | export default AnglePreview; 39 | -------------------------------------------------------------------------------- /src/components/Common/Button.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Button = styled.button` 4 | cursor: pointer; 5 | border: none; 6 | background: none; 7 | padding: 0; 8 | 9 | &:focus { 10 | outline: none; 11 | } 12 | ` 13 | 14 | export default Button 15 | -------------------------------------------------------------------------------- /src/components/Common/Checkbox.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | 5 | const Unchecked = ({ color }: { color: string }) => 6 | 7 | 14 | ; 15 | 16 | const Checked = ({ color }: { color: string }) => 17 | 18 | 23 | ; 24 | 25 | const Checkbox = ({ checked, color }: { checked: boolean, color: string }) => 26 | checked ? : ; 27 | 28 | export default Checkbox; 29 | -------------------------------------------------------------------------------- /src/components/Common/DashedBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // eslint-disable-line 2 | import styled from 'styled-components'; 3 | 4 | export default styled.div` 5 | background-image: linear-gradient(to right, #d9d9d9 40%, rgba(255, 255, 255, 0) 20%); 6 | background-position: top; 7 | background-size: 5px 1px; 8 | background-repeat: repeat-x; 9 | height: 1px; 10 | `; 11 | -------------------------------------------------------------------------------- /src/components/Common/Logo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Animate } from 'react-move'; 3 | 4 | import { INITIAL_STATE } from './../../store/stops/reducer'; 5 | 6 | const { values } = INITIAL_STATE; 7 | const newValues = [ 8 | ...Object.values(values), 9 | { 10 | '0': '#00E8F3', 11 | '100': '#96EA00' 12 | } 13 | ]; 14 | 15 | const getRandomColor = length => 16 | Array.from({ length }).map(() => { 17 | const colorSet = Object.values(newValues[Math.floor(Math.random() * newValues.length)]); 18 | return colorSet[Math.floor(Math.random() * colorSet.length)]; 19 | }); 20 | 21 | class Logo extends Component { 22 | state = { 23 | colors: { 24 | '0': getRandomColor(1)[0], 25 | '100': getRandomColor(1)[0] 26 | } 27 | }; 28 | 29 | componentDidMount() { 30 | this.interval = setInterval(this.updateColor, 1500); 31 | } 32 | 33 | componentWillUnmount() { 34 | delete this.interval; 35 | } 36 | 37 | updateColor = () => { 38 | const [color1, color2] = getRandomColor(2); 39 | 40 | const newColors = { 41 | '0': color1, 42 | '100': color2 43 | }; 44 | this.setState({ 45 | colors: newColors 46 | }); 47 | }; 48 | 49 | render() { 50 | const { colors } = this.state; 51 | 52 | return ( 53 | 54 | {data => 55 | 56 | 57 | 58 | {Object.keys(data.colors) 59 | .slice(0, 2) 60 | .map(stop => )} 61 | 62 | 63 | 64 | 68 | 69 | 70 | } 71 | 72 | ); 73 | } 74 | } 75 | 76 | export default Logo; 77 | -------------------------------------------------------------------------------- /src/components/Common/Triangle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' // eslint-disable-line no-unused-vars 2 | import styled from 'styled-components' 3 | 4 | const Triangle = styled.span` 5 | position: absolute; 6 | width: 0; 7 | height: 0; 8 | border-top: 10px solid white; 9 | 10 | ${({ right }) => (right ? 'border-left: 10px solid transparent;' : 'border-right: 10px solid transparent;')} 11 | bottom: -8px; 12 | ${({ right }) => (right ? 'right: 0;' : 'left: 0;')} 13 | ` 14 | 15 | export default Triangle 16 | -------------------------------------------------------------------------------- /src/components/Common/Typography.js: -------------------------------------------------------------------------------- 1 | import React from 'react' // eslint-disable-line 2 | import styled from 'styled-components' 3 | 4 | const Heading1 = styled.h1` 5 | font-size: 4.5rem; 6 | font-family: 'Poppins', sans-serif; 7 | font-weight: 700; 8 | ` 9 | 10 | const TextLG = styled.span` 11 | font-size: 2.2rem; 12 | font-family: 'Poppins', sans-serif; 13 | ` 14 | 15 | const TextMD = styled.span` 16 | font-size: 1.4rem; 17 | font-family: 'Poppins', sans-serif; 18 | ` 19 | 20 | const TextSM = styled.span` 21 | font-size: 1.2rem; 22 | font-family: 'Poppins', sans-serif; 23 | ` 24 | 25 | const TextXS = styled.span` 26 | font-size: 1rem; 27 | font-family: 'Poppins', sans-serif; 28 | ` 29 | 30 | export { Heading1, TextLG, TextMD, TextSM, TextXS } 31 | -------------------------------------------------------------------------------- /src/components/Common/UnfoldLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UnfoldLogo = () => 4 | 5 | 6 | 10 | 14 | 15 | ; 16 | 17 | export default UnfoldLogo; 18 | -------------------------------------------------------------------------------- /src/components/Common/index.js: -------------------------------------------------------------------------------- 1 | import Triangle from './Triangle'; 2 | import Button from './Button'; 3 | import Logo from './Logo'; 4 | import UnfoldLogo from './UnfoldLogo'; 5 | import DashedBar from './DashedBar'; 6 | import Checkbox from './Checkbox'; 7 | 8 | export { Triangle, Button, Logo, UnfoldLogo, DashedBar, Checkbox }; 9 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import john from "./../../assets/john.png"; 5 | import eddie from "./../../assets/eddie.png"; 6 | 7 | import { TextXS, TextSM } from "./../Common/Typography"; 8 | import { UnfoldLogo } from "./../Common/index"; 9 | import { ActionGroupItem, ActionGroupItemContainer } from "./../index"; 10 | 11 | const PhotoText = TextSM.extend` 12 | margin-left: 7px; 13 | `; 14 | 15 | const PrimaryContainer = styled.footer` 16 | margin: 0 auto 30px; 17 | padding: 0 20px; 18 | max-width: 1100px; 19 | `; 20 | 21 | const AdContainer = styled.div` 22 | margin-top: 24px; 23 | `; 24 | 25 | const CarbonAd = styled.div` 26 | margin-left: auto; 27 | 28 | @media (max-width: 620px) { 29 | margin-right: auto; 30 | } 31 | `; 32 | 33 | const Container = styled.div` 34 | display: flex; 35 | justify-content: space-between; 36 | align-items: center; 37 | flex-wrap: wrap; 38 | 39 | @media (max-width: 620px) { 40 | flex-direction: column; 41 | } 42 | `; 43 | 44 | const John = () => ( 45 | 55 | @johnkorzhuk 56 | john 57 | 58 | ); 59 | 60 | const Eddie = () => ( 61 | 71 | @lobanovskiy 72 | eddie 73 | 74 | ); 75 | 76 | const Footer = () => ( 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ©{new Date().getFullYear()} Grabient by 87 | 88 | 89 | 90 | 91 | 92 |