├── .editorconfig
├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── README.md
├── package-lock.json
├── package.json
├── src
├── .babelrc
├── App.js
├── App.test.js
├── actions
│ ├── ColorsActions.js
│ ├── GradientsActions.js
│ ├── SettingsActions.js
│ ├── __tests__
│ │ └── colors.test.js
│ └── index.js
├── components
│ ├── Icon.js
│ ├── background.js
│ ├── button.js
│ ├── circle.js
│ ├── color-picker.js
│ ├── color-rendered.js
│ ├── colors.js
│ ├── copy.js
│ ├── gradients.js
│ ├── nav.js
│ └── settings.js
├── constants
│ ├── ActionTypes.js
│ └── GlobalConstants.js
├── containers
│ ├── ColorsContainer.js
│ └── GradientsContainer.js
├── index.html
├── index.js
├── index.scss
├── layouts
│ └── index.js
├── reducers
│ ├── ColorsReducer.js
│ ├── GradientsReducer.js
│ ├── SettingsReducer.js
│ ├── __tests__
│ │ └── colors.test.js
│ └── index.js
├── routes.js
├── scss
│ ├── _globals.scss
│ ├── _mixins.scss
│ ├── background.scss
│ ├── button.scss
│ ├── circle.scss
│ ├── color-picker.scss
│ ├── color-rendered.scss
│ ├── colors.scss
│ ├── copy.scss
│ ├── nav.scss
│ └── settings.scss
├── selectors
│ ├── ColorsSelectors.js
│ ├── GradientsSelectors.js
│ └── SettingsSelectors.js
├── serviceWorker.js
├── store
│ ├── configureStore.js
│ └── mockStore.js
└── utils
│ ├── calculateStop.js
│ ├── copyToClipboard.js
│ ├── getRandomColor.js
│ ├── index.js
│ ├── localStorage.js
│ ├── offset.js
│ ├── preventClick.js
│ └── setGradient.js
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "tabWidth": 2,
4 | "arrowParens": "avoid",
5 | "bracketSpacing": true,
6 | "jsxBracketSameLine": false,
7 | "semi": true,
8 | "singleQuote": true,
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # any-color-react
2 | ### React + Redux.
3 | #### Inspired by [ColorSpark](https://colorspark.app/) and [Grabient](https://www.grabient.com/)
4 | #### Live demo [here](https://nttanh6299.github.io/any-color-react/#/)
5 | #### Usage
6 | 1. `npm i`
7 | 2. `npm run start` or `npm run build` for production build
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "any-color-react",
3 | "version": "0.1.0",
4 | "homepage": "https://nttanh6299.github.io/any-color-react",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server --config webpack.dev.js --open",
8 | "build": "webpack --config webpack.prod.js",
9 | "deploy": "gh-pages -d build",
10 | "test": "jest"
11 | },
12 | "dependencies": {
13 | "gh-pages": "^2.2.0",
14 | "prop-types": "^15.7.2",
15 | "react": "^16.13.1",
16 | "react-color": "^2.18.0",
17 | "react-dom": "^16.13.1",
18 | "react-redux": "^7.2.0",
19 | "react-router-dom": "^5.1.2",
20 | "redux": "^4.0.5",
21 | "redux-thunk": "^2.3.0",
22 | "reselect": "^4.0.0"
23 | },
24 | "devDependencies": {
25 | "@babel/core": "^7.8.7",
26 | "@babel/polyfill": "^7.8.7",
27 | "@babel/preset-env": "^7.8.7",
28 | "@babel/preset-react": "^7.8.3",
29 | "autoprefixer": "^9.7.4",
30 | "babel-loader": "^8.1.0",
31 | "css-loader": "^3.4.2",
32 | "dotenv": "^8.2.0",
33 | "file-loader": "^6.0.0",
34 | "html-loader": "^1.0.0",
35 | "html-webpack-plugin": "^3.2.0",
36 | "jest": "^25.2.4",
37 | "mini-css-extract-plugin": "^0.9.0",
38 | "node-sass": "^4.13.1",
39 | "optimize-css-assets-webpack-plugin": "^5.0.3",
40 | "path": "^0.12.7",
41 | "postcss-loader": "^3.0.0",
42 | "redux-mock-store": "^1.5.4",
43 | "sass-loader": "^8.0.2",
44 | "style-loader": "^1.1.3",
45 | "webpack": "^4.42.0",
46 | "webpack-cli": "^3.3.11",
47 | "webpack-dev-server": "^3.10.3",
48 | "webpack-merge": "^4.2.2"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { Switch } from 'react-router-dom';
3 | import { PublicRoute } from './layouts';
4 | import routes from './routes';
5 | import Nav from './components/nav';
6 |
7 | function App() {
8 | const renderRoutes = useMemo(() => {
9 | if (routes.length > 0) {
10 | return routes.map((route, index) => {
11 | return (
12 |
19 | );
20 | });
21 | }
22 | return null;
23 | }, [routes]);
24 |
25 | const renderNav = useMemo(() => , []);
26 |
27 | return (
28 | <>
29 | {renderNav}
30 | {renderRoutes}
31 | >
32 | );
33 | }
34 |
35 | export default App;
36 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | describe('test', () => {
2 | it('test with jest', () => {
3 | expect(1 + 1).toEqual(2);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/src/actions/ColorsActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | GENERATE_COLOR_REQUEST,
3 | GENERATE_COLOR_SUCCESS,
4 | CHANGE_COLOR,
5 | COPY_COLOR_TO_CLIPBOARD
6 | } from '../constants/ActionTypes';
7 | import { getRandomColor, copyTextToClipboard } from '../utils';
8 | import {
9 | getColors,
10 | getPrevColorIndex,
11 | getNextColorIndex
12 | } from '../selectors/ColorsSelectors';
13 |
14 | export const generateColorRequest = () => ({ type: GENERATE_COLOR_REQUEST });
15 |
16 | export const generateColorSuccess = color => ({
17 | type: GENERATE_COLOR_SUCCESS,
18 | color
19 | });
20 |
21 | export const changeColorIndex = index => ({ type: CHANGE_COLOR, index });
22 |
23 | export const copyToClipboard = successful => ({
24 | type: COPY_COLOR_TO_CLIPBOARD,
25 | successful
26 | });
27 |
28 | export const onGenerateColor = () => async (dispatch, getState) => {
29 | try {
30 | dispatch(generateColorRequest());
31 | const color = await getRandomColor();
32 | dispatch(generateColorSuccess(color));
33 | } catch (err) {
34 | console.log('generate color error:', err);
35 | }
36 | };
37 |
38 | export const prevColor = () => (dispatch, getState) => {
39 | const state = getState();
40 | const prevIndex = getPrevColorIndex(state);
41 | if (prevIndex !== -1) {
42 | dispatch(changeColorIndex(prevIndex));
43 | }
44 | };
45 |
46 | export const nextColor = () => (dispatch, getState) => {
47 | const state = getState();
48 | const nextIndex = getNextColorIndex(state);
49 | if (nextIndex !== -1) {
50 | dispatch(changeColorIndex(nextIndex));
51 | }
52 | };
53 |
54 | export const copyColorToClipboard = () => async (dispatch, getState) => {
55 | const state = getState();
56 | const colors = getColors(state);
57 | const hasItems = colors.list.length > 0;
58 | const currentIndex = colors.currentIndex;
59 |
60 | if (hasItems && currentIndex >= 0) {
61 | const successful = await copyTextToClipboard(colors.list[currentIndex]);
62 | dispatch(copyToClipboard(successful));
63 | }
64 | };
65 |
66 | export const generateColorIfNeeded = () => async (dispatch, getState) => {
67 | const state = getState();
68 | const colors = getColors(state);
69 | if (colors.list.length === 0) {
70 | dispatch(onGenerateColor());
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/src/actions/GradientsActions.js:
--------------------------------------------------------------------------------
1 | import {
2 | GENERATE_GRADIENT_REQUEST,
3 | GENERATE_GRADIENT_SUCCESS,
4 | CHANGE_GRADIENT,
5 | COPY_GRADIENT_TO_CLIPBOARD,
6 | ADD_NEW_COLOR,
7 | EDIT_ANGLE,
8 | CHANGE_GRADIENT_DIRECTION,
9 | TOGGLE_EDIT_COLOR_OF_GRADIENT,
10 | EDIT_COLOR_OF_GRADIENT,
11 | TOGGLE_SLIDER,
12 | START_UPDATE_COLOR_STOP,
13 | UPDATE_COLOR_STOP,
14 | DELETE_SELECTED_COLOR
15 | } from '../constants/ActionTypes';
16 | import { getRandomColor, copyTextToClipboard, setGradient } from '../utils';
17 | import {
18 | getPrevGradientIndex,
19 | getNextGradientIndex,
20 | getGradients
21 | } from '../selectors/GradientsSelectors';
22 | import { getSettings } from '../selectors/SettingsSelectors';
23 |
24 | const generateGradientRequest = () => ({ type: GENERATE_GRADIENT_REQUEST });
25 |
26 | const generateGradientSuccess = (colors, deg) => ({
27 | type: GENERATE_GRADIENT_SUCCESS,
28 | colors,
29 | deg
30 | });
31 |
32 | const changeGradientIndex = index => ({ type: CHANGE_GRADIENT, index });
33 |
34 | const copyToClipboard = successful => ({
35 | type: COPY_GRADIENT_TO_CLIPBOARD,
36 | successful
37 | });
38 |
39 | export const switchEditAngle = () => ({ type: EDIT_ANGLE });
40 |
41 | export const changeGradientDirection = deg => ({
42 | type: CHANGE_GRADIENT_DIRECTION,
43 | deg
44 | });
45 |
46 | export const onGenerateGradient = () => async dispatch => {
47 | dispatch(generateGradientRequest());
48 | Promise.all([getRandomColor(), getRandomColor()])
49 | .then(values => {
50 | const deg = Math.floor(Math.random() * 360);
51 | dispatch(generateGradientSuccess(values, deg));
52 | })
53 | .catch(err => console.log(err));
54 | };
55 |
56 | export const prevGradient = () => (dispatch, getState) => {
57 | const state = getState();
58 | const prevIndex = getPrevGradientIndex(state);
59 | if (prevIndex !== -1) {
60 | dispatch(changeGradientIndex(prevIndex));
61 | }
62 | };
63 |
64 | export const nextGradient = () => (dispatch, getState) => {
65 | const state = getState();
66 | const nextIndex = getNextGradientIndex(state);
67 | if (nextIndex !== -1) {
68 | dispatch(changeGradientIndex(nextIndex));
69 | }
70 | };
71 |
72 | export const copyGradientToClipboard = () => async (dispatch, getState) => {
73 | const state = getState();
74 | const gradients = getGradients(state);
75 | const { prefix, fallback } = getSettings(state);
76 |
77 | const hasItems = gradients.list.length > 0;
78 | const currentIndex = gradients.currentIndex;
79 | const colorsFromGradient = gradients.list[currentIndex];
80 |
81 | if (hasItems && currentIndex >= 0 && !!colorsFromGradient) {
82 | const successful = await copyTextToClipboard(
83 | setGradient(colorsFromGradient, prefix, fallback, true, true)
84 | );
85 | dispatch(copyToClipboard(successful));
86 | }
87 | };
88 |
89 | export const generateGradientIfNeeded = () => async (dispatch, getState) => {
90 | const state = getState();
91 | const gradients = getGradients(state);
92 | if (gradients.list.length === 0) {
93 | dispatch(onGenerateGradient());
94 | }
95 | };
96 |
97 | export const addNewColor = () => async (dispatch, getState) => {
98 | try {
99 | const state = getState();
100 | const gradients = getGradients(state);
101 | const currentIndex = gradients.currentIndex;
102 | const colorsFromGradient = gradients.list[currentIndex];
103 |
104 | if (colorsFromGradient && colorsFromGradient.colors.length < 5) {
105 | const color = await getRandomColor();
106 | dispatch({ type: ADD_NEW_COLOR, color });
107 | }
108 | } catch (err) {
109 | console.log('add new color error:', err);
110 | }
111 | };
112 |
113 | export const toggleEditColorOfGradient = colorIndex => ({
114 | type: TOGGLE_EDIT_COLOR_OF_GRADIENT,
115 | colorIndex
116 | });
117 |
118 | export const editColorOfGradient = color => ({
119 | type: EDIT_COLOR_OF_GRADIENT,
120 | color
121 | });
122 |
123 | export const toggleSlider = () => ({ type: TOGGLE_SLIDER });
124 |
125 | export const startUpdateColorStop = colorIndex => ({
126 | type: START_UPDATE_COLOR_STOP,
127 | colorIndex
128 | });
129 |
130 | export const updateColorStop = percent => ({
131 | type: UPDATE_COLOR_STOP,
132 | percent
133 | });
134 |
135 | export const deleteSelectedColor = () => ({ type: DELETE_SELECTED_COLOR });
136 |
--------------------------------------------------------------------------------
/src/actions/SettingsActions.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_PREFIX, TOGGLE_FALLBACK } from '../constants/ActionTypes';
2 | import { settingsSelector } from '../selectors/SettingsSelectors';
3 | import { setLocalStorage } from '../utils/localStorage';
4 |
5 | export const togglePrefix = () => (dispatch, getState) => {
6 | const state = getState();
7 | const settings = settingsSelector(state);
8 | const prefix = !settings.prefix;
9 |
10 | dispatch({ type: TOGGLE_PREFIX });
11 |
12 | setLocalStorage({ ...settings, prefix });
13 | };
14 |
15 | export const toggleFallback = () => (dispatch, getState) => {
16 | const state = getState();
17 | const settings = settingsSelector(state);
18 | const fallback = !settings.fallback;
19 |
20 | dispatch({ type: TOGGLE_FALLBACK });
21 |
22 | setLocalStorage({ ...settings, fallback });
23 | };
24 |
--------------------------------------------------------------------------------
/src/actions/__tests__/colors.test.js:
--------------------------------------------------------------------------------
1 | import * as actions from '../ColorsActions';
2 | import * as types from '../../constants/ActionTypes';
3 | import mockStore from '../../store/mockStore';
4 | import '@babel/polyfill';
5 |
6 | describe('Plain actions', () => {
7 | it('Should create an action to request to generate a color', () => {
8 | const expectedAction = { type: types.GENERATE_COLOR_REQUEST };
9 | expect(actions.generateColorRequest()).toEqual(expectedAction);
10 | });
11 |
12 | it('Should create an action for a color is generated successfully', () => {
13 | const expectedAction = {
14 | type: types.GENERATE_COLOR_SUCCESS,
15 | color: '#FFFFFF'
16 | };
17 | expect(actions.generateColorSuccess('#FFFFFF')).toEqual(expectedAction);
18 | });
19 |
20 | it('Should create an action to change the index of colors', () => {
21 | const expectedAction = {
22 | type: types.CHANGE_COLOR,
23 | index: 1
24 | };
25 | expect(actions.changeColorIndex(1)).toEqual(expectedAction);
26 | });
27 |
28 | it('Should create an action when the color is copied to clipboard', () => {
29 | const expectedAction = {
30 | type: types.COPY_COLOR_TO_CLIPBOARD,
31 | successful: true
32 | };
33 | expect(actions.copyToClipboard(true)).toEqual(expectedAction);
34 | });
35 | });
36 |
37 | describe('Async actions', () => {
38 | it('Should create GENERATE_COLOR_SUCCESS when generating a color has been done', () => {
39 | const initialState = {
40 | colors: {
41 | currentIndex: -1,
42 | list: []
43 | }
44 | };
45 | const store = mockStore(initialState);
46 | return store.dispatch(actions.onGenerateColor()).then(() => {
47 | expect(store.getActions().length).toEqual(2);
48 | });
49 | });
50 |
51 | it('Should go prev color when the current index is greater than -1', done => {
52 | const initialState = {
53 | colors: {
54 | currentIndex: 2,
55 | list: ['#FFFFFF', '#000000', '#00FF00']
56 | }
57 | };
58 | const store = mockStore(initialState);
59 |
60 | const expectedAction = { type: types.CHANGE_COLOR, index: 1 };
61 |
62 | store.dispatch(actions.prevColor());
63 | expect(store.getActions()).toEqual([expectedAction]);
64 | done();
65 | });
66 |
67 | it('Should not go prev color when the current index is equals to -1', done => {
68 | const initialState = {
69 | colors: {
70 | currentIndex: -1,
71 | list: []
72 | }
73 | };
74 | const store = mockStore(initialState);
75 |
76 | store.dispatch(actions.prevColor());
77 | expect(store.getActions()).toEqual([]);
78 | done();
79 | });
80 |
81 | it('Should go next color when the current index is less than the list length', done => {
82 | const initialState = {
83 | colors: {
84 | currentIndex: 0,
85 | list: ['#FFFFFF', '#000000']
86 | }
87 | };
88 | const store = mockStore(initialState);
89 |
90 | const expectedAction = { type: types.CHANGE_COLOR, index: 1 };
91 |
92 | store.dispatch(actions.nextColor());
93 | expect(store.getActions()).toEqual([expectedAction]);
94 | done();
95 | });
96 |
97 | it('Should not go next color when the current index is equals to the list length', done => {
98 | const initialState = {
99 | colors: {
100 | currentIndex: 1,
101 | list: ['#FFFFFF', '#000000']
102 | }
103 | };
104 | const store = mockStore(initialState);
105 |
106 | store.dispatch(actions.nextColor());
107 | expect(store.getActions()).toEqual([]);
108 | done();
109 | });
110 |
111 | it('Should generate new color when list is empty', done => {
112 | const initialState = {
113 | colors: {
114 | currentIndex: -1,
115 | list: []
116 | }
117 | };
118 | const store = mockStore(initialState);
119 |
120 | store.dispatch(actions.generateColorIfNeeded()).then(() => {
121 | expect(store.getActions().length).toEqual(2);
122 | done();
123 | });
124 | });
125 |
126 | it('Should not generate new color when list is not empty', done => {
127 | const initialState = {
128 | colors: {
129 | currentIndex: 0,
130 | list: ['#FFFFFF']
131 | }
132 | };
133 | const store = mockStore(initialState);
134 |
135 | store.dispatch(actions.generateColorIfNeeded());
136 | expect(store.getActions().length).toEqual(0);
137 | done();
138 | });
139 | });
140 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from './ColorsActions';
2 | export * from './GradientsActions';
3 | export * from './SettingsActions';
4 |
--------------------------------------------------------------------------------
/src/components/Icon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const propTypes = {
5 | children: PropTypes.node,
6 | className: PropTypes.string
7 | };
8 |
9 | const Icon = ({ children, className = '' }) => {
10 | return {children};
11 | };
12 |
13 | Icon.propTypes = propTypes;
14 |
15 | export default React.memo(Icon);
16 |
--------------------------------------------------------------------------------
/src/components/background.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const propTypes = {
5 | color: PropTypes.string,
6 | children: PropTypes.node
7 | };
8 |
9 | const Background = ({ color, children }) => {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
17 | Background.propTypes = propTypes;
18 |
19 | export default React.memo(Background);
20 |
--------------------------------------------------------------------------------
/src/components/button.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const propTypes = {
5 | children: PropTypes.node,
6 | className: PropTypes.string,
7 | style: PropTypes.object
8 | };
9 |
10 | const Button = ({ children, className = '', style, ...props }) => {
11 | return (
12 |
15 | );
16 | };
17 |
18 | Button.propTypes = propTypes;
19 |
20 | export default React.memo(Button);
21 |
--------------------------------------------------------------------------------
/src/components/circle.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { offset } from '../utils';
4 |
5 | const propTypes = {
6 | deg: PropTypes.number.isRequired,
7 | changeGradientDirection: PropTypes.func.isRequired,
8 | switchEditAngle: PropTypes.func.isRequired
9 | };
10 |
11 | const Circle = ({ deg, changeGradientDirection, switchEditAngle }) => {
12 | const [centerCircleX, setCenterCircleX] = useState(0);
13 | const [centerCircleY, setCenterCircleY] = useState(0);
14 | const handleRef = useRef(null);
15 |
16 | useEffect(() => {
17 | const { current } = handleRef;
18 | setCenterCircleX(offset(current, 'left'));
19 | setCenterCircleY(offset(current, 'top'));
20 |
21 | return () => {
22 | handleRef.current.offsetParent.removeEventListener(
23 | 'mousemove',
24 | onMouseMove
25 | );
26 | };
27 | }, []);
28 |
29 | //P1: center circle coords
30 | //P2: mouse coords
31 | // angle in radians: Math.atan2(p2.y - p1.y, p2.x - p1.x);
32 | // angle in degrees: Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI;
33 | const onMouseMove = e => {
34 | const { clientX, clientY } = e;
35 | const diffX = clientX - centerCircleX;
36 | const diffY = clientY - centerCircleY;
37 | let angle = Math.floor((Math.atan2(diffY, diffX) * 180) / Math.PI);
38 | angle = angle < 0 ? 360 + angle : angle;
39 | changeGradientDirection(angle);
40 | };
41 |
42 | return (
43 |
55 | );
56 | };
57 |
58 | Circle.propTypes = propTypes;
59 |
60 | export default React.memo(Circle);
61 |
--------------------------------------------------------------------------------
/src/components/color-picker.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { ChromePicker } from 'react-color';
4 |
5 | const propTypes = {
6 | color: PropTypes.string.isRequired,
7 | editColorOfGradient: PropTypes.func.isRequired,
8 | visible: PropTypes.bool.isRequired
9 | };
10 |
11 | const ColorPicker = ({ color, editColorOfGradient, visible }) => {
12 | const handleChange = colorSelected => {
13 | const { hex } = colorSelected;
14 | editColorOfGradient(hex);
15 | };
16 |
17 | if (!visible) {
18 | return null;
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | ColorPicker.propTypes = propTypes;
29 | ColorPicker.defaultProps = {
30 | color: '#fff',
31 | visible: false
32 | };
33 |
34 | export default React.memo(ColorPicker);
35 |
--------------------------------------------------------------------------------
/src/components/color-rendered.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useEffect, useRef } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Button from './button';
4 | import ColorPicker from './color-picker';
5 | import { offset, preventClick } from '../utils';
6 |
7 | const propTypes = {
8 | gradient: PropTypes.object,
9 | toggleEditColorOfGradient: PropTypes.func.isRequired,
10 | startUpdateColorStop: PropTypes.func.isRequired,
11 | updateColorStop: PropTypes.func.isRequired,
12 | editColorOfGradient: PropTypes.func.isRequired
13 | };
14 |
15 | const ColorRendered = ({
16 | gradient,
17 | toggleEditColorOfGradient,
18 | startUpdateColorStop,
19 | updateColorStop,
20 | editColorOfGradient
21 | }) => {
22 | const slider = useRef(null);
23 |
24 | useEffect(() => {
25 | if (!showSlider) {
26 | window.removeEventListener('mousemove', onMouseMove);
27 | window.removeEventListener('mouseup', onMouseUp);
28 | }
29 |
30 | return () => {
31 | window.removeEventListener('mousemove', onMouseMove);
32 | window.removeEventListener('mouseup', onMouseUp);
33 | };
34 | }, [showSlider]);
35 |
36 | const onMouseDown = index => () => {
37 | const { showSlider } = gradient;
38 | if (showSlider) {
39 | startUpdateColorStop(index);
40 | window.addEventListener('mouseup', onMouseUp);
41 | window.addEventListener('mousemove', onMouseMove);
42 | }
43 | };
44 |
45 | const onMouseMove = e => {
46 | const { clientX } = e;
47 | const { current } = slider;
48 | const diff = clientX - offset(current, 'left');
49 | const percent = Math.min(Math.max(diff / current.offsetWidth, 0), 1);
50 | updateColorStop(Math.floor(percent * 100));
51 | };
52 |
53 | const onMouseUp = () => {
54 | window.removeEventListener('mousemove', onMouseMove);
55 | window.removeEventListener('mouseup', onMouseUp);
56 | };
57 |
58 | const renderColor = useMemo(() => {
59 | const { colors, colorIndexEditing, showSlider, showHub } = gradient;
60 | return colors.map(({ color, stop }, index, { length }) => {
61 | const active =
62 | colorIndexEditing === index && showHub ? 'button--active' : '';
63 | const leftStop = showSlider ? stop : Math.floor((index * 100) / length);
64 |
65 | return (
66 |
75 |
86 |
91 |
92 | );
93 | });
94 | }, [gradient]);
95 |
96 | if (!gradient) {
97 | return null;
98 | }
99 |
100 | const { colors, showSlider } = gradient;
101 | const sliderWidth = showSlider ? 100 : colors.length * 25;
102 | const unit = showSlider ? '%' : 'px';
103 | const opacity = showSlider ? 1 : 0;
104 |
105 | return (
106 |
107 |
111 |
112 | {renderColor}
113 |
114 |
115 | );
116 | };
117 |
118 | ColorRendered.propTypes = propTypes;
119 | ColorRendered.defaultProps = { gradient: null };
120 |
121 | export default React.memo(ColorRendered);
122 |
--------------------------------------------------------------------------------
/src/components/colors.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Background from './background';
4 | import Button from './button';
5 | import Copy from './copy';
6 | import Icon from './Icon';
7 |
8 | const propTypes = {
9 | color: PropTypes.string,
10 | isCopied: PropTypes.bool.isRequired,
11 | onGenerateColor: PropTypes.func.isRequired,
12 | prevColor: PropTypes.func.isRequired,
13 | nextColor: PropTypes.func.isRequired,
14 | copyColorToClipboard: PropTypes.func.isRequired,
15 | generateColorIfNeeded: PropTypes.func.isRequired
16 | };
17 |
18 | const Colors = ({
19 | color,
20 | isCopied,
21 | onGenerateColor,
22 | prevColor,
23 | nextColor,
24 | copyColorToClipboard,
25 | generateColorIfNeeded
26 | }) => {
27 | useEffect(() => {
28 | generateColorIfNeeded();
29 | document.title = 'AnyColorReact - Colors';
30 | }, []);
31 |
32 | return (
33 |
34 |
35 |
36 | {color && (
37 |
38 | )}
39 |
40 |
{color}
41 |
42 |
48 |
51 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | Colors.propTypes = propTypes;
61 |
62 | export default Colors;
63 |
--------------------------------------------------------------------------------
/src/components/copy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Icon from './Icon';
4 |
5 | const propTypes = {
6 | copyToClipboard: PropTypes.func.isRequired,
7 | isCopied: PropTypes.bool.isRequired
8 | };
9 |
10 | const Copy = ({ copyToClipboard, isCopied }) => {
11 | return (
12 |
13 |
14 | {isCopied ? 'done' : 'code'}
15 | {isCopied ? 'Copied!' : 'Copy'}
16 |
17 |
18 | );
19 | };
20 |
21 | Copy.propTypes = propTypes;
22 |
23 | export default React.memo(Copy);
24 |
--------------------------------------------------------------------------------
/src/components/gradients.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Background from './background';
4 | import Button from './button';
5 | import Circle from './circle';
6 | import Copy from './copy';
7 | import Settings from './settings';
8 | import ColorRendered from './color-rendered';
9 | import Icon from './Icon';
10 | import { setGradient } from '../utils';
11 |
12 | const propTypes = {
13 | gradient: PropTypes.shape({
14 | colors: PropTypes.array,
15 | deg: PropTypes.number
16 | }),
17 | isCopied: PropTypes.bool.isRequired,
18 | onGenerateGradient: PropTypes.func.isRequired,
19 | prevGradient: PropTypes.func.isRequired,
20 | nextGradient: PropTypes.func.isRequired,
21 | generateGradientIfNeeded: PropTypes.func.isRequired,
22 | copyGradientToClipboard: PropTypes.func.isRequired,
23 | addNewColor: PropTypes.func.isRequired,
24 | editAngle: PropTypes.bool.isRequired,
25 | switchEditAngle: PropTypes.func.isRequired,
26 | changeGradientDirection: PropTypes.func.isRequired,
27 | prefix: PropTypes.bool.isRequired,
28 | fallback: PropTypes.bool.isRequired,
29 | togglePrefix: PropTypes.func.isRequired,
30 | toggleFallback: PropTypes.func.isRequired,
31 | toggleEditColorOfGradient: PropTypes.func.isRequired,
32 | editColorOfGradient: PropTypes.func.isRequired,
33 | toggleSlider: PropTypes.func.isRequired,
34 | startUpdateColorStop: PropTypes.func.isRequired,
35 | updateColorStop: PropTypes.func.isRequired,
36 | deleteSelectedColor: PropTypes.func.isRequired
37 | };
38 |
39 | const Gradients = ({
40 | gradient,
41 | isCopied,
42 | onGenerateGradient,
43 | prevGradient,
44 | nextGradient,
45 | generateGradientIfNeeded,
46 | copyGradientToClipboard,
47 | addNewColor,
48 | editAngle,
49 | switchEditAngle,
50 | changeGradientDirection,
51 | prefix,
52 | fallback,
53 | togglePrefix,
54 | toggleFallback,
55 | toggleEditColorOfGradient,
56 | editColorOfGradient,
57 | toggleSlider,
58 | startUpdateColorStop,
59 | updateColorStop,
60 | deleteSelectedColor
61 | }) => {
62 | useEffect(() => {
63 | generateGradientIfNeeded();
64 | }, []);
65 |
66 | if (!gradient) {
67 | return null;
68 | }
69 |
70 | const { deg, showHub, showSlider, colors } = gradient;
71 | const isDeleteColor = showHub && colors.length > 2;
72 |
73 | return (
74 |
75 |
76 |
82 |
83 | {!editAngle ? (
84 |
88 | ) : (
89 |
94 | )}
95 |
96 |
97 |
104 |
111 |
120 |
127 |
128 |
129 |
135 |
141 |
147 |
148 |
149 |
150 | );
151 | };
152 |
153 | Gradients.propTypes = propTypes;
154 | Gradients.defaultProps = {};
155 |
156 | export default Gradients;
157 |
--------------------------------------------------------------------------------
/src/components/nav.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, Route } from 'react-router-dom';
3 | import { HEADER_LINKS } from '../constants/GlobalConstants';
4 |
5 | const HeaderLink = ({ to, label, exact }) => (
6 | {
10 | return match ? (
11 | {label}
12 | ) : (
13 |
14 | {label}
15 |
16 | );
17 | }}
18 | />
19 | );
20 |
21 | const Nav = () => {
22 | return (
23 |
24 |
43 |
44 | );
45 | };
46 |
47 | export default React.memo(Nav);
48 |
--------------------------------------------------------------------------------
/src/components/settings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Icon from './Icon';
4 |
5 | const propTypes = {
6 | prefix: PropTypes.bool.isRequired,
7 | fallback: PropTypes.bool.isRequired,
8 | togglePrefix: PropTypes.func.isRequired,
9 | toggleFallback: PropTypes.func.isRequired
10 | };
11 |
12 | const Settings = ({ prefix, fallback, togglePrefix, toggleFallback }) => {
13 | return (
14 |
15 |
16 | Prefixes
17 |
18 | {prefix ? 'check_box' : 'check_box_outline_blank'}
19 |
20 |
21 |
22 | Fallback
23 |
24 | {fallback ? 'check_box' : 'check_box_outline_blank'}
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | Settings.propTypes = propTypes;
32 |
33 | export default React.memo(Settings);
34 |
--------------------------------------------------------------------------------
/src/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | //colors
2 | export const GENERATE_COLOR_REQUEST = 'GENERATE_COLOR_REQUEST';
3 | export const GENERATE_COLOR_SUCCESS = 'GENERATE_COLOR_SUCCESS';
4 | export const CHANGE_COLOR = 'CHANGE_COLOR';
5 | export const COPY_COLOR_TO_CLIPBOARD = 'COPY_COLOR_TO_CLIPBOARD';
6 |
7 | //gradients
8 | export const GENERATE_GRADIENT_REQUEST = 'GENERATE_GRADIENT_REQUEST';
9 | export const GENERATE_GRADIENT_SUCCESS = 'GENERATE_GRADIENT_SUCCESS';
10 | export const CHANGE_GRADIENT = 'CHANGE_GRADIENT';
11 | export const COPY_GRADIENT_TO_CLIPBOARD = 'COPY_GRADIENT_TO_CLIPBOARD';
12 | export const ADD_NEW_COLOR = 'ADD_NEW_COLOR';
13 | export const EDIT_ANGLE = 'EDIT_ANGLE';
14 | export const CHANGE_GRADIENT_DIRECTION = 'CHANGE_GRADIENT_DIRECTION';
15 | export const TOGGLE_EDIT_COLOR_OF_GRADIENT = 'TOGGLE_EDIT_COLOR_OF_GRADIENT';
16 | export const EDIT_COLOR_OF_GRADIENT = 'EDIT_COLOR_OF_GRADIENT';
17 | export const TOGGLE_SLIDER = 'TOGGLE_SLIDER';
18 | export const START_UPDATE_COLOR_STOP = 'START_UPDATE_COLOR_STOP';
19 | export const UPDATE_COLOR_STOP = 'UPDATE_COLOR_STOP';
20 | export const DELETE_SELECTED_COLOR = 'DELETE_SELECTED_COLOR';
21 |
22 | //settings
23 | export const TOGGLE_PREFIX = 'TOGGLE_PREFIXES';
24 | export const TOGGLE_FALLBACK = 'TOGGLE_FALLBACK';
25 |
--------------------------------------------------------------------------------
/src/constants/GlobalConstants.js:
--------------------------------------------------------------------------------
1 | export const HEADER_LINKS = [
2 | {
3 | key: 'gradients',
4 | label: 'Gradients',
5 | to: '/',
6 | exact: true
7 | },
8 | {
9 | key: 'colors',
10 | label: 'Colors',
11 | to: '/colors',
12 | exact: true
13 | }
14 | ];
15 |
16 | export const LOCALSTORAGE_KEY = 'config';
17 |
--------------------------------------------------------------------------------
/src/containers/ColorsContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import Colors from '../components/colors';
4 | import {
5 | onGenerateColor,
6 | prevColor,
7 | nextColor,
8 | copyColorToClipboard,
9 | generateColorIfNeeded
10 | } from '../actions';
11 | import { colorsSelector } from '../selectors/ColorsSelectors';
12 |
13 | const ColorsContainer = () => {
14 | const colors = useSelector(colorsSelector);
15 | const dispatch = useDispatch();
16 |
17 | const OnGenerateColor = useCallback(() => dispatch(onGenerateColor()), [
18 | dispatch
19 | ]);
20 | const PrevColor = useCallback(() => dispatch(prevColor()), [dispatch]);
21 | const NextColor = useCallback(() => dispatch(nextColor()), [dispatch]);
22 | const CopyColorToClipboard = useCallback(
23 | () => dispatch(copyColorToClipboard()),
24 | [dispatch]
25 | );
26 | const GenerateColorIfNeeded = useCallback(
27 | () => dispatch(generateColorIfNeeded()),
28 | [dispatch]
29 | );
30 |
31 | return (
32 |
40 | );
41 | };
42 |
43 | export default ColorsContainer;
44 |
--------------------------------------------------------------------------------
/src/containers/GradientsContainer.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import {
4 | onGenerateGradient,
5 | prevGradient,
6 | nextGradient,
7 | generateGradientIfNeeded,
8 | copyGradientToClipboard,
9 | addNewColor,
10 | switchEditAngle,
11 | changeGradientDirection,
12 | togglePrefix,
13 | toggleFallback,
14 | toggleEditColorOfGradient,
15 | editColorOfGradient,
16 | toggleSlider,
17 | startUpdateColorStop,
18 | updateColorStop,
19 | deleteSelectedColor
20 | } from '../actions';
21 | import Gradients from '../components/gradients';
22 | import { gradientsSelector } from '../selectors/GradientsSelectors';
23 | import { settingsSelector } from '../selectors/SettingsSelectors';
24 |
25 | const GradientsContainer = () => {
26 | const gradients = useSelector(gradientsSelector);
27 | const settings = useSelector(settingsSelector);
28 | const dispatch = useDispatch();
29 |
30 | const OnGenerateGradient = useCallback(() => dispatch(onGenerateGradient()), [
31 | dispatch
32 | ]);
33 | const PrevGradient = useCallback(() => dispatch(prevGradient()), [dispatch]);
34 | const NextGradient = useCallback(() => dispatch(nextGradient()), [dispatch]);
35 | const GenerateGradientIfNeeded = useCallback(
36 | () => dispatch(generateGradientIfNeeded()),
37 | [dispatch]
38 | );
39 | const CopyGradientToClipboard = useCallback(
40 | () => dispatch(copyGradientToClipboard()),
41 | [dispatch]
42 | );
43 | const AddNewColor = useCallback(() => dispatch(addNewColor()), [dispatch]);
44 | const SwitchEditAngle = useCallback(() => dispatch(switchEditAngle()), [
45 | dispatch
46 | ]);
47 | const ChangeGradientDirection = useCallback(
48 | deg => dispatch(changeGradientDirection(deg)),
49 | [dispatch]
50 | );
51 | const TogglePrefix = useCallback(() => dispatch(togglePrefix()), [dispatch]);
52 | const ToggleFallback = useCallback(() => dispatch(toggleFallback()), [
53 | dispatch
54 | ]);
55 | const ToggleEditColorOfGradient = useCallback(
56 | colorIndex => dispatch(toggleEditColorOfGradient(colorIndex)),
57 | [dispatch]
58 | );
59 | const EditColorOfGradient = useCallback(
60 | (color, stop) => dispatch(editColorOfGradient(color, stop)),
61 | [dispatch]
62 | );
63 | const ToggleSlider = useCallback(() => dispatch(toggleSlider()), [dispatch]);
64 | const StartUpdateColorStop = useCallback(
65 | colorIndex => dispatch(startUpdateColorStop(colorIndex)),
66 | [dispatch]
67 | );
68 | const UpdateColorStop = useCallback(
69 | percent => dispatch(updateColorStop(percent)),
70 | [dispatch]
71 | );
72 | const DeleteSelectedColor = useCallback(
73 | () => dispatch(deleteSelectedColor()),
74 | [dispatch]
75 | );
76 |
77 | return (
78 |
98 | );
99 | };
100 |
101 | export default GradientsContainer;
102 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
15 |
19 | AnyColorReact
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import '@babel/polyfill';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import './index.scss';
5 | import App from './App';
6 | import { Provider } from 'react-redux';
7 | import { HashRouter } from 'react-router-dom';
8 | import configureStore from './store/configureStore';
9 | import { getLocalStorage } from './utils/localStorage';
10 |
11 | const persisted = getLocalStorage();
12 |
13 | const Main = () => (
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | ReactDOM.render(, document.getElementById('root'));
22 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import './scss/mixins';
2 | @import './scss/globals';
3 |
4 | @import './scss/nav';
5 | @import './scss/colors';
6 | @import './scss/background';
7 | @import './scss/button';
8 | @import './scss/circle';
9 | @import './scss/copy';
10 | @import './scss/settings';
11 | @import './scss/color-picker';
12 | @import './scss/color-rendered';
13 |
--------------------------------------------------------------------------------
/src/layouts/index.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import { Route } from 'react-router-dom';
3 |
4 | const PublicRoute = ({ component: Component, layout: Layout, ...rest }) => (
5 | (
8 |
9 |
10 |
11 | )}
12 | />
13 | );
14 |
15 | const PublicLayout = ({ children }) => (
16 | <>
17 |
20 |
21 |
22 | }
23 | >
24 | {children}
25 |
26 | >
27 | );
28 |
29 | export { PublicRoute, PublicLayout };
30 |
--------------------------------------------------------------------------------
/src/reducers/ColorsReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | GENERATE_COLOR_REQUEST,
3 | GENERATE_COLOR_SUCCESS,
4 | CHANGE_COLOR,
5 | COPY_COLOR_TO_CLIPBOARD
6 | } from '../constants/ActionTypes';
7 |
8 | const initialState = {
9 | loading: false,
10 | currentIndex: -1,
11 | list: [],
12 | isCopied: false
13 | };
14 |
15 | export default function(state = initialState, action) {
16 | switch (action.type) {
17 | case GENERATE_COLOR_REQUEST:
18 | return {
19 | ...state,
20 | loading: true
21 | };
22 | case GENERATE_COLOR_SUCCESS:
23 | return {
24 | ...state,
25 | loading: false,
26 | isCopied: false,
27 | currentIndex: state.list.length,
28 | list: [...state.list, action.color]
29 | };
30 | case CHANGE_COLOR:
31 | return {
32 | ...state,
33 | currentIndex: action.index,
34 | isCopied: false
35 | };
36 | case COPY_COLOR_TO_CLIPBOARD:
37 | return {
38 | ...state,
39 | isCopied: action.successful
40 | };
41 | default:
42 | return state;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/reducers/GradientsReducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | GENERATE_GRADIENT_REQUEST,
3 | GENERATE_GRADIENT_SUCCESS,
4 | CHANGE_GRADIENT,
5 | COPY_GRADIENT_TO_CLIPBOARD,
6 | ADD_NEW_COLOR,
7 | EDIT_ANGLE,
8 | CHANGE_GRADIENT_DIRECTION,
9 | TOGGLE_EDIT_COLOR_OF_GRADIENT,
10 | EDIT_COLOR_OF_GRADIENT,
11 | TOGGLE_SLIDER,
12 | START_UPDATE_COLOR_STOP,
13 | UPDATE_COLOR_STOP,
14 | TOGGLE_PREFIX,
15 | TOGGLE_FALLBACK,
16 | DELETE_SELECTED_COLOR
17 | } from '../constants/ActionTypes';
18 | import { calculateStop } from '../utils';
19 |
20 | const initialState = {
21 | loading: false,
22 | currentIndex: -1,
23 | list: [],
24 | isCopied: false,
25 | editAngle: false
26 | };
27 |
28 | const initialGradient = {
29 | colors: [],
30 | deg: 0,
31 | colorIndexEditing: -1,
32 | showSlider: false,
33 | showHub: false
34 | };
35 |
36 | function gradient(state = initialGradient, action) {
37 | switch (action.type) {
38 | case GENERATE_GRADIENT_SUCCESS:
39 | return {
40 | ...state,
41 | colors: state.colors
42 | .concat(action.colors)
43 | .map((color, index, colors) => ({
44 | color,
45 | stop: calculateStop(100, colors.length, index)
46 | })),
47 | deg: action.deg
48 | };
49 | case ADD_NEW_COLOR:
50 | return {
51 | ...state,
52 | colors: state.colors
53 | .concat({ color: action.color })
54 | .map((color, index, colors) => ({
55 | ...color,
56 | stop: calculateStop(100, colors.length, index)
57 | })),
58 | showHub: false
59 | };
60 | case CHANGE_GRADIENT_DIRECTION:
61 | return {
62 | ...state,
63 | deg: action.deg
64 | };
65 | case TOGGLE_EDIT_COLOR_OF_GRADIENT:
66 | return {
67 | ...state,
68 | colorIndexEditing: action.colorIndex,
69 | showHub:
70 | state.colorIndexEditing !== action.colorIndex ? true : !state.showHub
71 | };
72 | case EDIT_COLOR_OF_GRADIENT:
73 | return {
74 | ...state,
75 | colors: state.colors.map((color, index) => {
76 | return index === state.colorIndexEditing
77 | ? { ...color, color: action.color }
78 | : color;
79 | })
80 | };
81 | case TOGGLE_SLIDER:
82 | return {
83 | ...state,
84 | showSlider: !state.showSlider,
85 | showHub: false,
86 | colors: [].concat(
87 | state.colors.sort((left, right) => left.stop - right.stop)
88 | )
89 | };
90 | case START_UPDATE_COLOR_STOP:
91 | return {
92 | ...state,
93 | colorIndexEditing: action.colorIndex
94 | };
95 | case UPDATE_COLOR_STOP:
96 | return {
97 | ...state,
98 | colors: state.colors.map((color, index) => {
99 | return index === state.colorIndexEditing
100 | ? { ...color, stop: action.percent }
101 | : color;
102 | })
103 | };
104 | case DELETE_SELECTED_COLOR:
105 | return {
106 | ...state,
107 | colors: [
108 | ...state.colors.filter(
109 | (_, index) => index !== state.colorIndexEditing
110 | )
111 | ],
112 | colorIndexEditing: -1,
113 | showHub: false
114 | };
115 | default:
116 | return state;
117 | }
118 | }
119 |
120 | export default function (state = initialState, action) {
121 | switch (action.type) {
122 | case GENERATE_GRADIENT_REQUEST:
123 | return {
124 | ...state,
125 | loading: true
126 | };
127 | case GENERATE_GRADIENT_SUCCESS:
128 | return {
129 | ...state,
130 | loading: false,
131 | isCopied: false,
132 | editAngle: false,
133 | currentIndex: state.list.length,
134 | list: [...state.list, gradient(undefined, action)]
135 | };
136 | case CHANGE_GRADIENT:
137 | return {
138 | ...state,
139 | currentIndex: action.index,
140 | isCopied: false,
141 | editAngle: false
142 | };
143 | case COPY_GRADIENT_TO_CLIPBOARD:
144 | return {
145 | ...state,
146 | isCopied: true,
147 | editAngle: false
148 | };
149 | case START_UPDATE_COLOR_STOP:
150 | case UPDATE_COLOR_STOP:
151 | case ADD_NEW_COLOR:
152 | case EDIT_COLOR_OF_GRADIENT:
153 | case TOGGLE_EDIT_COLOR_OF_GRADIENT:
154 | case CHANGE_GRADIENT_DIRECTION:
155 | case TOGGLE_SLIDER:
156 | case DELETE_SELECTED_COLOR:
157 | return {
158 | ...state,
159 | isCopied: false,
160 | list: state.list.map((item, index) => {
161 | return index === state.currentIndex
162 | ? gradient(state.list[state.currentIndex], action)
163 | : item;
164 | })
165 | };
166 | case EDIT_ANGLE:
167 | return {
168 | ...state,
169 | editAngle: !state.editAngle,
170 | isCopied: false
171 | };
172 | case TOGGLE_PREFIX:
173 | case TOGGLE_FALLBACK:
174 | return {
175 | ...state,
176 | isCopied: false
177 | };
178 | default:
179 | return state;
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/reducers/SettingsReducer.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_PREFIX, TOGGLE_FALLBACK } from '../constants/ActionTypes';
2 |
3 | const initialState = {
4 | prefix: false,
5 | fallback: false
6 | };
7 |
8 | export default function(state = initialState, action) {
9 | switch (action.type) {
10 | case TOGGLE_PREFIX:
11 | return {
12 | ...state,
13 | prefix: !state.prefix
14 | };
15 | case TOGGLE_FALLBACK:
16 | return {
17 | ...state,
18 | fallback: !state.fallback
19 | };
20 | default:
21 | return state;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/reducers/__tests__/colors.test.js:
--------------------------------------------------------------------------------
1 | import reducer from '../ColorsReducer';
2 | import * as types from '../../constants/ActionTypes';
3 |
4 | describe('Colors Reducer', () => {
5 | it('Should return the initial state', () => {
6 | const expectedState = {
7 | loading: false,
8 | currentIndex: -1,
9 | list: [],
10 | isCopied: false
11 | };
12 | expect(reducer(undefined, {})).toEqual(expectedState);
13 | });
14 |
15 | it('Should add new color to the list', () => {
16 | const color = '#FFFFFF';
17 | const expectedState = {
18 | loading: false,
19 | currentIndex: 0,
20 | list: [color],
21 | isCopied: false
22 | };
23 |
24 | expect(
25 | reducer(undefined, { type: types.GENERATE_COLOR_SUCCESS, color })
26 | ).toEqual(expectedState);
27 | });
28 |
29 | it('Should change the index of the list', () => {
30 | const indexChanged = 1;
31 | const currentState = {
32 | loading: false,
33 | currentIndex: 0,
34 | list: ['#FFFFFF', '#000000'],
35 | isCopied: false
36 | };
37 |
38 | expect(
39 | reducer(currentState, { type: types.CHANGE_COLOR, index: indexChanged })
40 | ).toEqual({ ...currentState, currentIndex: indexChanged });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import ColorsReducer from './ColorsReducer';
3 | import GradientsReducer from './GradientsReducer';
4 | import SettingsReducer from './SettingsReducer';
5 |
6 | export default combineReducers({
7 | colors: ColorsReducer,
8 | gradients: GradientsReducer,
9 | settings: SettingsReducer
10 | });
11 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react';
2 | import { PublicLayout } from './layouts';
3 |
4 | const ColorsContainer = lazy(() => import('./containers/ColorsContainer'));
5 | const GradientsContainer = lazy(() =>
6 | import('./containers/GradientsContainer')
7 | );
8 |
9 | const routes = [
10 | {
11 | path: '/colors',
12 | exact: true,
13 | layout: PublicLayout,
14 | component: ColorsContainer
15 | },
16 | {
17 | path: '/',
18 | exact: true,
19 | layout: PublicLayout,
20 | component: GradientsContainer
21 | }
22 | ];
23 |
24 | export default routes;
25 |
--------------------------------------------------------------------------------
/src/scss/_globals.scss:
--------------------------------------------------------------------------------
1 | $dark: #333;
2 |
3 | $gray: #bbb;
4 | $grayDark: darken($gray, 5);
5 | $grayLight: lighten($gray, 10);
6 | $grayLighter: lighten($gray, 20);
7 |
8 | $navHeight: 100px;
9 |
10 | * {
11 | margin: 0;
12 | padding: 0;
13 | }
14 |
15 | *,
16 | *:before,
17 | *:after {
18 | box-sizing: border-box;
19 | user-select: none;
20 | }
21 |
22 | a {
23 | color: $dark;
24 | text-decoration: none;
25 |
26 | &:hover {
27 | text-decoration: underline;
28 | }
29 | }
30 |
31 | button,
32 | input,
33 | textarea {
34 | color: $dark;
35 | font-family: 'Poppins', sans-serif;
36 | }
37 |
38 | button {
39 | cursor: pointer;
40 |
41 | @include m-mobile {
42 | cursor: default;
43 | }
44 | }
45 |
46 | body {
47 | color: $dark;
48 | font-size: 16px;
49 | font-family: 'Poppins', sans-serif;
50 | position: relative;
51 | }
52 |
53 | .icon {
54 | font-size: 16px;
55 | margin: 0 4px;
56 | }
57 |
58 | .container {
59 | width: 100%;
60 | display: flex;
61 | justify-content: center;
62 | align-items: center;
63 | }
64 |
65 | .inner {
66 | width: 100%;
67 | max-width: 440px;
68 | text-align: center;
69 | margin: 0 auto;
70 | }
71 |
72 | .suspense {
73 | height: calc(100vh - #{$navHeight});
74 | display: flex;
75 | justify-content: center;
76 | align-items: center;
77 |
78 | &__spinner {
79 | background-color: #ff4136;
80 | width: 100px;
81 | height: 100px;
82 | margin-top: -#{$navHeight};
83 | animation: spinner 0.9s infinite linear;
84 |
85 | @keyframes spinner {
86 | from {
87 | transform: rotate(0);
88 | }
89 | to {
90 | transform: rotate(360deg);
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | $mobileWidth: 320px;
2 | $tabletWidth: 768px;
3 |
4 | @mixin m-mobile {
5 | @media only screen and (min-width: $mobileWidth) and (max-width: $tabletWidth) {
6 | @content;
7 | }
8 | }
9 |
10 | @mixin awesome-hover(
11 | $direction: top,
12 | $height: 100%,
13 | $backgroundColor: $dark,
14 | $duration: 0.65s
15 | ) {
16 | position: relative;
17 |
18 | &:before {
19 | content: '';
20 | position: absolute;
21 | #{$direction}: 0;
22 | right: 0;
23 | width: 0;
24 | height: $height;
25 | background: $backgroundColor;
26 | transition: width $duration cubic-bezier(0.51, 0.18, 0, 0.98);
27 | z-index: -101;
28 | }
29 |
30 | &:hover {
31 | &:before {
32 | width: 100%;
33 | left: 0;
34 | }
35 | }
36 | }
37 |
38 | @mixin fade-animation($duration: 0.3s, $timingFucntion: linear) {
39 | animation: fade $duration $timingFucntion;
40 |
41 | @keyframes fade {
42 | from {
43 | opacity: 0;
44 | }
45 | to {
46 | opacity: 1;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/scss/background.scss:
--------------------------------------------------------------------------------
1 | .background {
2 | position: relative;
3 | width: 100%;
4 | height: 400px;
5 | overflow: hidden;
6 | z-index: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/scss/button.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | font-size: 14px;
3 | display: flex;
4 | align-items: center;
5 | border: none;
6 | outline: none;
7 | padding: 8px 0;
8 |
9 | &:active {
10 | transform: translate(0.5px, 0.5px);
11 | }
12 | }
13 |
14 | .button--active {
15 | transform: scale(1.2);
16 | transition: transform 0.2s;
17 | }
18 |
19 | .awesome-hover {
20 | @include awesome-hover(top, 100%);
21 | }
22 |
--------------------------------------------------------------------------------
/src/scss/circle.scss:
--------------------------------------------------------------------------------
1 | .circle {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | overflow: hidden;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | z-index: 2;
10 | background-color: rgba(0, 0, 0, 0.2);
11 | }
12 |
13 | .circle__cover {
14 | width: 300px;
15 | height: 300px;
16 | border-radius: 50%;
17 | border: 3px solid $grayLighter;
18 | }
19 |
20 | .circle__handle {
21 | position: absolute;
22 | top: 50%;
23 | left: 50%;
24 | width: 147px;
25 | height: 3px;
26 | background: $grayLighter;
27 | transform-origin: 0 0;
28 | }
29 |
--------------------------------------------------------------------------------
/src/scss/color-picker.scss:
--------------------------------------------------------------------------------
1 | .color-picker {
2 | width: 100%;
3 | position: absolute;
4 | bottom: 100%;
5 | right: 0;
6 | margin-bottom: 10px;
7 | z-index: 100;
8 | }
9 |
--------------------------------------------------------------------------------
/src/scss/color-rendered.scss:
--------------------------------------------------------------------------------
1 | .color-rendered {
2 | flex: 1;
3 | height: 20px;
4 | margin: 0 26px 0 8px;
5 | display: flex;
6 | justify-content: flex-start;
7 | }
8 |
9 | .color-rendered__slider {
10 | height: 100%;
11 | position: relative;
12 | display: flex;
13 | align-items: center;
14 | }
15 |
16 | .color-rendered__color {
17 | width: 20px;
18 | height: 20px;
19 | margin-right: 10px;
20 | padding: 0;
21 | margin: 0;
22 | border-radius: 50%;
23 | }
24 |
25 | .color-rendered__fill {
26 | width: 100%;
27 | height: 2px;
28 | background: $gray;
29 | }
30 |
31 | .color-rendered__color-wrapper {
32 | position: absolute;
33 | }
34 |
--------------------------------------------------------------------------------
/src/scss/colors.scss:
--------------------------------------------------------------------------------
1 | .colors {
2 | width: 100%;
3 | overflow: hidden;
4 | @include fade-animation(0.65s, ease-out);
5 | }
6 |
7 | .colors__value {
8 | width: 100%;
9 | height: 50px;
10 | display: flex;
11 | align-items: center;
12 | text-transform: uppercase;
13 | font-size: 18px;
14 | }
15 |
16 | .colors__actions {
17 | width: 100%;
18 | display: flex;
19 | justify-content: space-between;
20 | align-items: center;
21 | flex-wrap: wrap;
22 | }
23 |
24 | .colors__action {
25 | flex: 0 0 49%;
26 | justify-content: center;
27 | margin-bottom: 8px;
28 | z-index: 1;
29 | transition: color 0.2s linear;
30 | transition-delay: 0.3s;
31 |
32 | &:hover {
33 | color: $grayLighter;
34 | }
35 | }
36 |
37 | .colors__action--generate {
38 | flex: 0 0 100%;
39 | }
40 |
41 | .colors__handle {
42 | width: 100%;
43 | height: 50px;
44 | display: flex;
45 | justify-content: flex-end;
46 | align-items: center;
47 | }
48 |
49 | .colors__deg {
50 | width: 70px;
51 | padding: 2px 4px !important;
52 | letter-spacing: 1px;
53 | background: transparent;
54 | font-size: 20px;
55 | }
56 |
57 | .colors__deg--active {
58 | background: $dark;
59 | color: $grayLighter;
60 | }
61 |
--------------------------------------------------------------------------------
/src/scss/copy.scss:
--------------------------------------------------------------------------------
1 | .copy {
2 | opacity: 0;
3 | position: relative;
4 | width: 100%;
5 | height: 100%;
6 | display: flex;
7 | justify-content: center;
8 | align-items: center;
9 | color: #fff;
10 | background-color: rgba(0, 0, 0, 0.4);
11 | cursor: pointer;
12 | z-index: 1;
13 | transition: all 0.2s linear;
14 |
15 | &:hover {
16 | opacity: 1;
17 | }
18 | }
19 |
20 | .copy__text {
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | font-size: 16px;
25 | }
26 |
--------------------------------------------------------------------------------
/src/scss/nav.scss:
--------------------------------------------------------------------------------
1 | .nav {
2 | width: 100%;
3 | height: $navHeight;
4 | display: flex;
5 | align-items: center;
6 | @include fade-animation(0.65s, ease-out);
7 | }
8 |
9 | .nav__inner {
10 | width: 90%;
11 | margin: 0 auto;
12 |
13 | display: flex;
14 | justify-content: space-between;
15 |
16 | @include m-mobile {
17 | width: 100%;
18 | flex-direction: column;
19 | }
20 | }
21 |
22 | .nav__section--logo {
23 | justify-content: flex-start;
24 | }
25 |
26 | .nav__section--menu {
27 | justify-content: center;
28 | }
29 |
30 | .nav__section--switch {
31 | justify-content: flex-end;
32 | }
33 |
34 | .nav__section {
35 | flex: 1;
36 | display: flex;
37 |
38 | @include m-mobile {
39 | justify-content: center;
40 |
41 | & + & {
42 | margin-top: 10px;
43 | }
44 | }
45 | }
46 |
47 | .nav__logo {
48 | position: relative;
49 | font-size: 18px;
50 | text-transform: uppercase;
51 | word-spacing: -4px;
52 | color: $dark;
53 | letter-spacing: -1px;
54 | transition: 0.2s;
55 |
56 | &:hover {
57 | text-decoration: none;
58 | word-spacing: 0px;
59 | }
60 | }
61 |
62 | .nav__item {
63 | font-size: 16px;
64 | color: $gray;
65 |
66 | & + & {
67 | margin-left: 10px;
68 | }
69 |
70 | &:hover {
71 | text-decoration: none;
72 | color: $dark;
73 | }
74 | }
75 |
76 | .nav__item--active {
77 | color: $dark;
78 | cursor: pointer;
79 |
80 | @include m-mobile {
81 | cursor: default;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/scss/settings.scss:
--------------------------------------------------------------------------------
1 | .settings {
2 | width: 100%;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | margin-bottom: 10px;
7 | font-size: 14px;
8 | }
9 |
10 | .settings__section {
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | cursor: pointer;
15 |
16 | & + & {
17 | margin-left: 20px;
18 | }
19 |
20 | &:active {
21 | transform: translate(0.5px, 0.5px);
22 | }
23 |
24 | @include m-mobile {
25 | cursor: default;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/selectors/ColorsSelectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const getColors = state => state.colors;
4 |
5 | export const colorsSelector = createSelector(getColors, colors => ({
6 | isCopied: colors.isCopied,
7 | color: colors.list[colors.currentIndex]
8 | }));
9 |
10 | export const getPrevColorIndex = createSelector(getColors, colors => {
11 | const currentIndex = colors.currentIndex;
12 | return currentIndex <= 0 ? -1 : currentIndex - 1;
13 | });
14 |
15 | export const getNextColorIndex = createSelector(getColors, colors => {
16 | const listLength = colors.list.length;
17 | const currentIndex = colors.currentIndex;
18 | return currentIndex === listLength - 1 ? -1 : currentIndex + 1;
19 | });
20 |
--------------------------------------------------------------------------------
/src/selectors/GradientsSelectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const getGradients = state => state.gradients;
4 |
5 | export const gradientsSelector = createSelector(getGradients, gradients => ({
6 | isCopied: gradients.isCopied,
7 | editAngle: gradients.editAngle,
8 | gradient: gradients.list[gradients.currentIndex]
9 | }));
10 |
11 | export const getPrevGradientIndex = createSelector(getGradients, gradients => {
12 | const currentIndex = gradients.currentIndex;
13 | return currentIndex <= 0 ? -1 : currentIndex - 1;
14 | });
15 |
16 | export const getNextGradientIndex = createSelector(getGradients, gradients => {
17 | const listLength = gradients.list.length;
18 | const currentIndex = gradients.currentIndex;
19 | return currentIndex === listLength - 1 ? -1 : currentIndex + 1;
20 | });
21 |
--------------------------------------------------------------------------------
/src/selectors/SettingsSelectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | export const getSettings = state => state.settings;
4 |
5 | export const settingsSelector = createSelector(getSettings, settings => ({
6 | prefix: settings.prefix,
7 | fallback: settings.fallback
8 | }));
9 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' }
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, compose, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import rootReducer from '../reducers';
4 |
5 | const middlewares = [thunk];
6 |
7 | const composeEnhancers =
8 | typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
9 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
10 | // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
11 | })
12 | : compose;
13 |
14 | //const composeEnhancers = compose;
15 |
16 | const enhancer = composeEnhancers(applyMiddleware(...middlewares));
17 |
18 | export default function configureStore(initialState) {
19 | const store = createStore(rootReducer, initialState, enhancer);
20 | return store;
21 | }
22 |
--------------------------------------------------------------------------------
/src/store/mockStore.js:
--------------------------------------------------------------------------------
1 | import configureMockStore from 'redux-mock-store';
2 | import thunk from 'redux-thunk';
3 |
4 | const middlewares = [thunk];
5 |
6 | const mockStore = configureMockStore(middlewares);
7 |
8 | export default mockStore;
9 |
--------------------------------------------------------------------------------
/src/utils/calculateStop.js:
--------------------------------------------------------------------------------
1 | export function calculateStop(max, length, point) {
2 | return Math.ceil((point * max) / (length - 1));
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/copyToClipboard.js:
--------------------------------------------------------------------------------
1 | function fallbackCopyTextToClipboard(text) {
2 | const textArea = document.createElement('textarea');
3 |
4 | textArea.value = text;
5 |
6 | // Avoid scrolling to bottom
7 | textArea.style.top = '0';
8 | textArea.style.left = '0';
9 | textArea.style.position = 'fixed';
10 |
11 | document.body.appendChild(textArea);
12 | textArea.focus();
13 | textArea.select();
14 |
15 | let successful = false;
16 | try {
17 | successful = document.execCommand('copy');
18 | } catch (err) {
19 | console.log('Fallback: Oops, unable to copy', err);
20 | }
21 |
22 | document.body.removeChild(textArea);
23 |
24 | return successful;
25 | }
26 |
27 | export async function copyTextToClipboard(text) {
28 | if (!navigator.clipboard) {
29 | return fallbackCopyTextToClipboard(text);
30 | }
31 | return navigator.clipboard
32 | .writeText(text)
33 | .then(() => true)
34 | .catch(() => false);
35 | }
36 |
--------------------------------------------------------------------------------
/src/utils/getRandomColor.js:
--------------------------------------------------------------------------------
1 | export function getRandomColor() {
2 | return new Promise(resolve => {
3 | const letters = '0123456789ABCDEF';
4 | let color = '#';
5 | for (let i = 0; i < 6; i++) {
6 | color += letters[Math.floor(Math.random() * 16)];
7 | }
8 | resolve(color);
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export { getRandomColor } from './getRandomColor';
2 | export { copyTextToClipboard } from './copyToClipboard';
3 | export { setGradient } from './setGradient';
4 | export { offset } from './offset';
5 | export { calculateStop } from './calculateStop';
6 | export { preventClick } from './preventClick';
7 |
--------------------------------------------------------------------------------
/src/utils/localStorage.js:
--------------------------------------------------------------------------------
1 | import { LOCALSTORAGE_KEY } from '../constants/GlobalConstants';
2 |
3 | export function getLocalStorage() {
4 | try {
5 | const serializedData = window.localStorage.getItem(LOCALSTORAGE_KEY);
6 | if (!serializedData) {
7 | return undefined;
8 | }
9 | return JSON.parse(serializedData);
10 | } catch (e) {
11 | return undefined;
12 | }
13 | }
14 |
15 | export function setLocalStorage(data) {
16 | try {
17 | const serializedData = JSON.stringify({
18 | settings: {
19 | ...data
20 | }
21 | });
22 | window.localStorage.setItem(LOCALSTORAGE_KEY, serializedData);
23 | } catch (e) {
24 | console.log('Set local storage failed');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/offset.js:
--------------------------------------------------------------------------------
1 | export function offset(e, direction = 'top') {
2 | let el = e;
3 | let x = direction === 'left' ? e.offsetLeft : e.offsetTop;
4 | while (el.offsetParent) {
5 | x +=
6 | direction === 'left'
7 | ? el.offsetParent.offsetLeft
8 | : el.offsetParent.offsetTop;
9 | el = el.offsetParent;
10 | }
11 |
12 | return x;
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/preventClick.js:
--------------------------------------------------------------------------------
1 | export function preventClick(e) {
2 | e.preventDefault();
3 | e.stopPropagation();
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/setGradient.js:
--------------------------------------------------------------------------------
1 | /*
2 | background: #FFF; //fallback
3 | background: linear-gradient(180deg, #FFF, #000);
4 | background: -webkit-linear-gradient(180deg, #FFF, #000);
5 | background: -o-linear-gradient(180deg, #FFF, #000);
6 | */
7 |
8 | /*
9 | gradient:
10 | colors: [{ color: '#FFF', stop: 0 }],
11 | deg: 100deg
12 | */
13 |
14 | function setValues(colors) {
15 | const copyArr = [].concat(colors);
16 |
17 | return copyArr
18 | .sort((left, right) => left.stop - right.stop)
19 | .map(item => `${item.color} ${item.stop}%`);
20 | }
21 |
22 | function setAttributeName(colors, attrName) {
23 | if (!attrName) {
24 | return colors;
25 | }
26 | return colors.map(color => `background: ${color}`);
27 | }
28 |
29 | function setSemicolon(colors, semicolon) {
30 | if (!semicolon) {
31 | return colors;
32 | }
33 | return colors.map(color => `${color};`);
34 | }
35 |
36 | function setIfPrefix(colors, deg, prefix) {
37 | const result = [];
38 | if (prefix) {
39 | const prefixes = ['-webkit-', '-o-'];
40 | for (let i = 0; i < prefixes.length; i++) {
41 | result.push(`${prefixes[i]}linear-gradient(${deg}deg, ${colors})`);
42 | }
43 | }
44 | result.push(`linear-gradient(${deg}deg, ${colors})`);
45 | return result;
46 | }
47 |
48 | function setIfFallback(color, fallback) {
49 | if (!fallback) {
50 | return [];
51 | }
52 | return [color];
53 | }
54 |
55 | export function setGradient(
56 | gradient = {},
57 | prefix = false,
58 | fallback = false,
59 | attrName = false,
60 | semicolon = false
61 | ) {
62 | if (!gradient || (gradient && !gradient.colors)) {
63 | return null;
64 | }
65 | const { colors, deg } = gradient;
66 |
67 | return setSemicolon(
68 | setAttributeName(
69 | setIfFallback(colors[0].color, fallback).concat(
70 | setIfPrefix(setValues(colors), deg, prefix)
71 | ),
72 | attrName
73 | ),
74 | semicolon
75 | ).join('\n');
76 | }
77 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const dotenv = require('dotenv').config({ path: '.env' });
4 |
5 | module.exports = {
6 | entry: {
7 | main: './src/index.js',
8 | vendor: [
9 | 'react',
10 | 'react-dom',
11 | 'prop-types',
12 | 'react-redux',
13 | 'redux',
14 | 'redux-thunk',
15 | 'reselect',
16 | 'react-color'
17 | ]
18 | },
19 | output: {
20 | path: path.resolve(__dirname, 'build'),
21 | filename: 'js/[name].js'
22 | },
23 | module: {
24 | rules: [
25 | {
26 | test: /\.jsx?$/,
27 | exclude: /node_modules/,
28 | loader: 'babel-loader'
29 | },
30 | {
31 | test: /\.html$/,
32 | loader: 'html-loader'
33 | },
34 | {
35 | test: /\.(png|jpe?g|gif|svg|woff|woff2|eot|ttf|wav|mp3|ico)$/,
36 | use: {
37 | loader: 'file-loader',
38 | options: {
39 | name: '[name].[hash].[ext]',
40 | outputPath: 'img'
41 | }
42 | }
43 | }
44 | ]
45 | },
46 | resolve: {
47 | modules: [path.resolve('./src'), path.resolve('./node_modules')],
48 | extensions: ['.js', '.jsx']
49 | },
50 | plugins: [
51 | new webpack.DefinePlugin({
52 | 'process.env': JSON.stringify(dotenv.parsed)
53 | })
54 | ]
55 | };
56 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const common = require('./webpack.common');
2 | const merge = require('webpack-merge');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const autoprefixer = require('autoprefixer');
5 |
6 | module.exports = merge(common, {
7 | mode: 'development',
8 | module: {
9 | rules: [
10 | {
11 | test: /\.scss$/,
12 | use: [
13 | { loader: 'style-loader' },
14 | { loader: 'css-loader' },
15 | {
16 | loader: 'postcss-loader',
17 | options: {
18 | plugins: () => [
19 | autoprefixer({ overrideBrowserslist: ['> 1%', 'IE >= 10'] })
20 | ]
21 | }
22 | },
23 | { loader: 'sass-loader' }
24 | ]
25 | }
26 | ]
27 | },
28 | plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })]
29 | });
30 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const common = require('./webpack.common');
2 | const merge = require('webpack-merge');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const autoprefixer = require('autoprefixer');
6 | const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
7 | const TerserPlugin = require('terser-webpack-plugin');
8 | const webpack = require('webpack');
9 |
10 | module.exports = merge(common, {
11 | mode: 'production',
12 | module: {
13 | rules: [
14 | {
15 | test: /\.scss$/,
16 | use: [
17 | { loader: MiniCssExtractPlugin.loader },
18 | { loader: 'css-loader' },
19 | {
20 | loader: 'postcss-loader',
21 | options: {
22 | plugins: () => [
23 | autoprefixer({ overrideBrowserslist: ['> 1%', 'IE >= 10'] })
24 | ]
25 | }
26 | },
27 | { loader: 'sass-loader' }
28 | ]
29 | }
30 | ]
31 | },
32 | plugins: [
33 | new MiniCssExtractPlugin({ filename: 'css/main.css' }),
34 | new webpack.DefinePlugin({
35 | 'process.env.NODE_ENV': JSON.stringify('production')
36 | })
37 | ],
38 | optimization: {
39 | minimizer: [
40 | //optimize css
41 | new OptimizeCssAssetsWebpackPlugin(),
42 | //optimize js
43 | new TerserPlugin(),
44 | //optimize html
45 | new HtmlWebpackPlugin({
46 | template: './src/index.html',
47 | minify: {
48 | removeAttributeQuotes: true,
49 | collapseWhitespace: true,
50 | removeComments: true
51 | }
52 | })
53 | ],
54 | runtimeChunk: false
55 | }
56 | });
57 |
--------------------------------------------------------------------------------