diff --git a/README.md b/README.md
index e69de29..0527753 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+## About The Project
+
+Pixel Artify is a web-based tool for transforming images into pixel art. Try it out at [https://pixelartify.com](https://pixelartify.com)!
+
+![Demo Screenshot](client/src/assets/images/demo-screenshot.png)
+
+
+## Getting Started
+
+### Prerequisites
+
+This project requires Node.js which can be installed [here](https://nodejs.org/en/).
+
+### Development Setup
+
+1. Clone the repo:
+
+```sh
+
+git clone https://github.com/shannonlui/pixel-artify.git
+
+```
+
+2. Install the required dependencies in the client directory:
+
+```sh
+
+cd client
+npm install
+
+```
+
+4. Run the client app in development mode:
+
+```js
+
+npm start
+
+```
+
+
+## Features
+- [x] Pixelate uploaded images with customizable pixel size
+- [x] Adjust contrast, brightness, saturation, and color palette
+- [x] Paint, erase and color selection on canvas
+- [ ] Resizable canvas and image export
+- [ ] Zooming and panning the canvas
+- [ ] Undo or redo changes
+
+
+## Acknowledgments
+This project was built using [React.js](https://reactjs.org/), Redux, and the following libraries:
+
+* [Color Thief](https://github.com/lokesh/color-thief)
+* [FileSaver.js](https://github.com/eligrey/FileSaver.js/)
+* [Pica](https://github.com/nodeca/pica)
+* [React Color](https://github.com/casesandberg/react-color)
+* [React Icons](https://github.com/react-icons/react-icons)
+
diff --git a/client/package.json b/client/package.json
index 277809c..92391fa 100644
--- a/client/package.json
+++ b/client/package.json
@@ -3,9 +3,12 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "file-saver": "^2.0.2",
+ "file-saver": "^2.0.5",
+ "pica": "^9.0.1",
"react": "^16.8.6",
+ "react-color": "^2.19.3",
"react-dom": "^16.8.6",
+ "react-icons": "^4.3.1",
"react-redux": "^7.0.3",
"react-router-dom": "^5.0.0",
"react-scripts": "3.0.0",
diff --git a/client/src/assets/images/demo-screenshot.png b/client/src/assets/images/demo-screenshot.png
new file mode 100644
index 0000000..c537569
Binary files /dev/null and b/client/src/assets/images/demo-screenshot.png differ
diff --git a/client/src/assets/images/logo-left.png b/client/src/assets/images/logo-left.png
new file mode 100644
index 0000000..90c3d81
Binary files /dev/null and b/client/src/assets/images/logo-left.png differ
diff --git a/client/src/assets/images/logo-right.png b/client/src/assets/images/logo-right.png
new file mode 100644
index 0000000..2fb4958
Binary files /dev/null and b/client/src/assets/images/logo-right.png differ
diff --git a/client/src/assets/images/logo.png b/client/src/assets/images/logo.png
new file mode 100644
index 0000000..40e50e4
Binary files /dev/null and b/client/src/assets/images/logo.png differ
diff --git a/client/src/components/Button/Button.js b/client/src/components/Button/Button.js
new file mode 100644
index 0000000..325b1be
--- /dev/null
+++ b/client/src/components/Button/Button.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import styles from './Button.module.css';
+
+function Button(props) {
+ const { children, className, primary, secondary, small, ...otherProps } = props;
+ const classNames = [styles.button, className, primary && styles.primary,
+ secondary && styles.secondary, small && styles.small];
+
+ return (
+
+ );
+}
+
+export default Button;
\ No newline at end of file
diff --git a/client/src/components/Button/Button.module.css b/client/src/components/Button/Button.module.css
new file mode 100644
index 0000000..ad3ba1e
--- /dev/null
+++ b/client/src/components/Button/Button.module.css
@@ -0,0 +1,31 @@
+.button {
+ background-color: #ef6461;
+ border: none;
+ border-radius: 5px;
+ padding: 5px 10px;
+ color: white;
+ font-weight: bold;
+ text-transform: uppercase;
+ cursor: pointer;
+}
+
+.primary {
+ background-color: #1c9197;
+}
+
+.secondary {
+ background-color: grey;
+}
+
+.small {
+ font-size: 0.8em;
+}
+
+.label {
+ display: flex;
+ align-items: center;
+}
+
+.label svg {
+ margin-right: 5px;
+}
\ No newline at end of file
diff --git a/client/src/components/FormControl/FormControl.module.css b/client/src/components/FormControl/FormControl.module.css
index 862ed7a..1118cd2 100644
--- a/client/src/components/FormControl/FormControl.module.css
+++ b/client/src/components/FormControl/FormControl.module.css
@@ -1,12 +1,12 @@
.control {
color: white;
- margin-bottom: 64px;
+ margin-bottom: 40px;
}
.control .label {
display: flex;
align-items: center;
justify-content: space-between;
- margin-bottom: 10px;
+ margin-bottom: 2px;
font-weight: bold;
}
\ No newline at end of file
diff --git a/client/src/components/Loading/Loading.module.css b/client/src/components/Loading/Loading.module.css
index 10f5545..d31a4aa 100644
--- a/client/src/components/Loading/Loading.module.css
+++ b/client/src/components/Loading/Loading.module.css
@@ -1,5 +1,13 @@
.loading {
text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ transform: -webkit-translate(-50%, -50%);
+ transform: -moz-translate(-50%, -50%);
+ transform: -ms-translate(-50%, -50%);
+ z-index: 9999;
}
.text {
diff --git a/client/src/components/Modal/Modal.js b/client/src/components/Modal/Modal.js
new file mode 100644
index 0000000..2a19267
--- /dev/null
+++ b/client/src/components/Modal/Modal.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { FiX } from 'react-icons/fi';
+
+import styles from './Modal.module.css';
+import Button from '../Button/Button';
+
+function Modal(props) {
+ return (
+
+
e.stopPropagation()}>
+
+ {
+ props.header &&
+ {props.header}
+
+ }
+ { props.header &&
}
+
+ {props.children}
+
+
+
+
+
+
+
+ );
+}
+
+export default Modal;
\ No newline at end of file
diff --git a/client/src/components/Modal/Modal.module.css b/client/src/components/Modal/Modal.module.css
new file mode 100644
index 0000000..72cd18e
--- /dev/null
+++ b/client/src/components/Modal/Modal.module.css
@@ -0,0 +1,66 @@
+.background {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.6);
+ z-index: 20;
+}
+
+.modal {
+ position: relative;
+ background: #444;
+ min-width: 250px;
+ width: 40%;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ z-index: 100;
+ border-radius: 5px;
+ color: white;
+ overflow-y: scroll;
+}
+
+.header {
+ font-size: 1.2em;
+ font-weight: bold;
+ padding: 24px;
+ margin-right: 24px;
+ color: #ddd;
+}
+
+.closeBtn {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ font-size: 1.4em;
+ cursor: pointer;
+ color: white;
+ background: none;
+ border: none;
+}
+
+.body {
+ padding: 24px;
+ margin-bottom: 60px;
+ min-height: 100px;
+ height: 50%;
+ overflow: auto;
+}
+
+.footer {
+ position: absolute;
+ bottom: 24px;
+ right: 24px;
+}
+
+.footer button {
+ margin-left: 12px;
+}
+
+@media only screen and (max-width: 400px) {
+ .header {
+ font-size: 1em;
+ }
+}
\ No newline at end of file
diff --git a/client/src/components/SliderControl/SliderControl.module.css b/client/src/components/SliderControl/SliderControl.module.css
index 3e61172..494718c 100644
--- a/client/src/components/SliderControl/SliderControl.module.css
+++ b/client/src/components/SliderControl/SliderControl.module.css
@@ -6,6 +6,7 @@
height: 8px;
outline: none;
border: none;
+ background: #ddd;
}
.slider::-webkit-slider-thumb {
diff --git a/client/src/components/Toolbar/Toolbar.js b/client/src/components/Toolbar/Toolbar.js
index 93fc866..7e24ca1 100644
--- a/client/src/components/Toolbar/Toolbar.js
+++ b/client/src/components/Toolbar/Toolbar.js
@@ -9,7 +9,11 @@ const classes = {
const toolbar = (props) => (
- pixel artify
+
+
+ pixel artify
+
+
);
diff --git a/client/src/components/Toolbar/Toolbar.module.css b/client/src/components/Toolbar/Toolbar.module.css
index 4ba366e..ece8cd2 100644
--- a/client/src/components/Toolbar/Toolbar.module.css
+++ b/client/src/components/Toolbar/Toolbar.module.css
@@ -14,7 +14,7 @@
.logo {
font-family: 'VT323', serif;
- font-size: 46px;
+ font-size: 38px;
margin-top: -5px;
color:white;
text-decoration: none;
@@ -30,8 +30,19 @@
color: #ef6461;
}
+.logoLeft {
+ width: 20px;
+ margin-right: 10px;
+}
+
+.logoRight {
+ width: 20px;
+ margin-left: 8px;
+}
+
@media only screen and (max-width: 768px) {
.toolbar {
justify-content: center;
+ padding: 0;
}
}
\ No newline at end of file
diff --git a/client/src/constants/constants.js b/client/src/constants/constants.js
new file mode 100644
index 0000000..a0193b3
--- /dev/null
+++ b/client/src/constants/constants.js
@@ -0,0 +1,5 @@
+export const TOOL_TYPES = {
+ PAINT: "PAINT",
+ COLOR_PICK: "COLOR_PICK",
+ ERASE: "ERASE"
+}
\ No newline at end of file
diff --git a/client/src/pages/ImageEditor/Canvas/Canvas.js b/client/src/pages/ImageEditor/Canvas/Canvas.js
index b5faef7..7864f4d 100644
--- a/client/src/pages/ImageEditor/Canvas/Canvas.js
+++ b/client/src/pages/ImageEditor/Canvas/Canvas.js
@@ -1,31 +1,190 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { saveAs } from 'file-saver';
+import Pica from 'pica';
import styles from './Canvas.module.css';
import * as actions from '../../../store/actions';
-import { getColorDifference } from '../../../utils/colorDifference';
+import { getClosestColor, convertRgbToHex, adjustImageColors } from '../../../utils/colorDifference';
+import { TOOL_TYPES } from '../../../constants/constants';
+import { isDevEnv } from '../../../utils/utility';
class Canvas extends Component {
constructor(props) {
super(props);
+ this.state = {
+ img: null,
+ isPainting: false
+ };
+ this.grid = React.createRef();
this.canvas = React.createRef();
+ this.mouseLayer = React.createRef();
}
componentDidMount() {
this.props.setExportImage(this.saveCanvas);
+ this.props.setResetCanvas(this.resetCanvas);
- let img = this.props.img;
- img.onload = () => {
+ this.props.origImg.onload = () => {
+ this.resizeImage(this.props.origImg);
+ this.drawGrid();
+ }
+
+ // Add listeners for painting on the canvas using a mouse (from user input)
+ this.mouseLayer.current.addEventListener('mousedown', this.handeStartPainting);
+ this.mouseLayer.current.addEventListener('mouseup', this.handleStopPainting);
+ this.mouseLayer.current.addEventListener('mousemove', this.handleContinuePainting);
+ this.mouseLayer.current.addEventListener("mouseout", this.handleMouseOut);
+
+ // Trigger a confirmation dialog to ask user if they really want to leave the page
+ if (!isDevEnv())
+ window.onbeforeunload = () => true;
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.pixelSize !== prevProps.pixelSize || this.props.contrast !== prevProps.contrast
+ || this.props.brightness !== prevProps.brightness || this.props.saturation !== prevProps.saturation
+ || this.props.colorCount !== prevProps.colorCount)
+ {
+ this.resetCanvas();
+ }
+ }
+
+ componentWillUnmount() {
+ // Remove the trigger for confirming if user really want to leave the page
+ window.onbeforeunload = null;
+ }
+
+ resetCanvas = () => {
+ // Redraw the pixelated image. This will clear any painting done by the user!
+ this.pixelate(this.state.img, this.props.pixelSize);
+ this.applyColorAdjustments();
+ }
+
+ handeStartPainting = (event) => {
+ if (!this.props.isPaintEnabled)
+ return;
+ if (this.props.toolType === TOOL_TYPES.COLOR_PICK) {
+ // Get the color from the pixel that was touched
+ const ctx = this.canvas.current.getContext('2d');
+ const coord = this.getPosition(event);
+ const imgData = ctx.getImageData(coord.x, coord.y, 1, 1).data;
+ const hex = convertRgbToHex(imgData[0], imgData[1], imgData[2]);
+ this.props.onChangeColor(hex);
+ } else {
+ // Start painting
+ this.setState({isPainting: true});
+ this.paintOnCanvas(event);
+ }
+ }
+
+ handleStopPainting = (event) => {
+ this.setState({isPainting: false});
+ }
+
+ handleContinuePainting = (event) => {
+ if (!this.props.isPaintEnabled)
+ return;
+ if (this.state.isPainting) {
+ this.paintOnCanvas(event);
+ }
+ if (this.props.toolType !== TOOL_TYPES.COLOR_PICK) {
+ // Clear the canvas for the mouse layer
+ const mouseCanvas = this.mouseLayer.current;
+ const mouseCtx = mouseCanvas.getContext('2d');
+ mouseCtx.clearRect(0, 0, mouseCanvas.width, mouseCanvas.height);
+ // Draw a transparent rectangle to preview where the mouse will paint
+ this.drawSquare(event, mouseCtx, "rgba(0, 0, 0, 0.3)");
+ }
+ }
+
+ handleMouseOut = (event) => {
+ // TODO: add helper function for clearing canvas
+ const mouseCanvas = this.mouseLayer.current;
+ const mouseCtx = mouseCanvas.getContext('2d');
+ mouseCtx.clearRect(0, 0, mouseCanvas.width, mouseCanvas.height);
+ }
+
+ getPosition = (event) => {
+ var rect = this.canvas.current.getBoundingClientRect();
+ return {
+ x: event.clientX - rect.left,
+ y: event.clientY - rect.top
+ };
+ }
+
+ paintOnCanvas = (event) => {
+ const ctx = this.canvas.current.getContext('2d');
+ const color = this.props.toolType === TOOL_TYPES.PAINT ? this.props.paintColor : null;
+ this.drawSquare(event, ctx, color);
+ }
+
+ drawSquare = (event, ctx, color) => {
+ let { pixelSize, brushSize } = this.props;
+ const length = brushSize * pixelSize;
+ const coord = this.getPosition(event);
+ const offset = Math.floor(brushSize / 2) * pixelSize;
+ const x = Math.floor(coord.x / pixelSize) * pixelSize - offset;
+ const y = Math.floor(coord.y / pixelSize) * pixelSize - offset;
+ if (color == null) {
+ ctx.clearRect(x, y, length, length);
+ } else {
+ ctx.fillStyle = color;
+ ctx.fillRect(x, y, length, length);
+ }
+ }
+
+ resizeImage = (img) => {
+ const canvas = this.canvas.current;
+ // TODO: do not hardcode these numbers here
+ const isMobile = window.innerWidth <= 700;
+ const maxWidth = isMobile ? (window.innerWidth - 20) : (window.innerWidth - 340);
+ const maxHeight = isMobile ? 280 : (window.innerHeight - 20);
+ if (img.width > maxWidth || img.height > maxHeight) {
+ const ratio = Math.max(img.width / maxWidth, img.height / maxHeight);
+ this.setCanvasSize(Math.round(img.width / ratio), Math.round(img.height / ratio));
+ Pica().resize(img, canvas)
+ .then(result => this.setResizedImage(result.toDataURL()));
+ } else {
+ this.setCanvasSize(img.width, img.height);
+ this.setResizedImage(img.src);
+ }
+ }
+
+ setCanvasSize = (width, height) => {
+ const canvases = [this.canvas.current, this.mouseLayer.current, this.grid.current];
+ for (var c of canvases) {
+ c.width = width;
+ c.height = height;
+ }
+ }
+
+ setResizedImage = (imgSrc) => {
+ const resizedImg = new Image();
+ resizedImg.src = imgSrc;
+ resizedImg.onload = () => {
+ this.setState({img: resizedImg});
+ this.pixelate(resizedImg, this.props.pixelSize);
this.props.onLoadImageSuccess();
- this.pixelate(img, +this.props.pixelSize);
}
}
- componentDidUpdate() {
- this.pixelate(this.props.img, +this.props.pixelSize);
- this.adjustColors();
+ drawGrid = () => {
+ const { pixelSize } = this.props;
+ const canvas = this.grid.current;
+ const ctx = canvas.getContext('2d');
+ const cols = Math.floor(canvas.width, pixelSize);
+ const rows = Math.floor(canvas.height, pixelSize);
+ // Clear the canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ // Draw a checkerboard pattern to be the background of the main canvas
+ for (let y = 0; y < rows; y++) {
+ for (let x = 0; x < cols; x++) {
+ ctx.fillStyle = ["#888", "#666"][(x + y) % 2]; // Alternate the fill color
+ ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
+ }
+ }
}
pixelate = (img, pixelSize) => {
@@ -54,7 +213,7 @@ class Canvas extends Component {
let alpha = imgData[pos + 3];
if (alpha > 0) alpha = 255;
if (this.props.palette && this.props.palette.length > 0) {
- [red, green, blue] = this.getClosestColor(this.props.palette, [red, green, blue]);
+ [red, green, blue] = getClosestColor(this.props.palette, [red, green, blue]);
}
ctx.fillStyle = `rgba(${red}, ${green}, ${blue}, ${alpha})`;
ctx.fillRect(x, y, pixelSize, pixelSize);
@@ -63,78 +222,49 @@ class Canvas extends Component {
}
saveCanvas = () => {
- var ua = window.navigator.userAgent;
- var iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i);
- var webkit = !!ua.match(/WebKit/i);
- var iOSSafari = iOS && webkit && !ua.match(/CriOS/i);
const canvasURL = this.canvas.current.toDataURL('image/png');
- if (iOSSafari) {
- window.open(canvasURL, '_blank');
- } else {
- saveAs(canvasURL, 'pixelartify.png');
- }
+ saveAs(canvasURL, 'pixelartify.png');
}
- getClosestColor(colors, target) {
- let minDiff = Number.MAX_SAFE_INTEGER;
- let closest = null;
- const length = colors.length;
- for (let i = 0; i < length; i++) {
- let diff = getColorDifference(target, colors[i]);
- if (diff <= minDiff) {
- minDiff = diff;
- closest = colors[i];
- }
- }
- return closest;
- }
-
- adjustColors = () => {
+ applyColorAdjustments = () => {
const canvas = this.canvas.current;
const ctx = canvas.getContext('2d');
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const d = imgData.data;
-
- const saturation = (this.props.saturation / 100) + 1;
- const brightness = this.props.brightness * 0.75;
- const contrast = (this.props.contrast / 100) + 1;
- const con = 128 * (1 - contrast);
-
- // Adjust the saturation, brightness, and contrast of each pixel
- for (let i = 0; i < d.length; i += 4) {
- const gray = d[i] * 0.3086 + d[i + 1] * 0.6094 + d[i + 2] * 0.0820;
- const sat = gray * (1 - saturation);
-
- d[i] = (d[i] * saturation + sat + brightness) * contrast + con;
- d[i + 1] = (d[i + 1] * saturation + sat + brightness) * contrast + con;
- d[i + 2] = (d[i + 2] * saturation + sat + brightness) * contrast + con;
- }
-
+ adjustImageColors(imgData.data, this.props.saturation, this.props.brightness, this.props.contrast);
ctx.putImageData(imgData, 0, 0);
}
render() {
return(
-
+
+
+
+
+
);
}
}
const mapStateToProps = state => {
return {
- img: state.image,
+ origImg: state.image,
+ isPaintEnabled: state.isPaintEnabled,
+ paintColor: state.paintColor,
pixelSize: state.pixelSize,
+ brushSize: state.brushSize,
contrast: state.contrast,
brightness: state.brightness,
saturation: state.saturation,
colorCount: state.colorCount,
- palette: state.colorPalette
+ palette: state.colorPalette,
+ toolType: state.toolType,
};
};
const mapDispatchToProps = dispatch => {
return {
onLoadImageSuccess: () => dispatch(actions.loadImageSuccess()),
+ onChangeColor: (color) => dispatch(actions.updatePaintColor(color)),
};
};
diff --git a/client/src/pages/ImageEditor/Canvas/Canvas.module.css b/client/src/pages/ImageEditor/Canvas/Canvas.module.css
index 101e564..5161b70 100644
--- a/client/src/pages/ImageEditor/Canvas/Canvas.module.css
+++ b/client/src/pages/ImageEditor/Canvas/Canvas.module.css
@@ -1,4 +1,9 @@
.canvas {
- max-width: 85%;
- max-height: 85%;
+ position: absolute;
+}
+
+@media only screen and (max-width: 700px) {
+ .hideOnSmallScreen {
+ opacity: 0;
+ }
}
\ No newline at end of file
diff --git a/client/src/pages/ImageEditor/Controls/Controls.js b/client/src/pages/ImageEditor/Controls/Controls.js
index 84369c7..d717bbf 100644
--- a/client/src/pages/ImageEditor/Controls/Controls.js
+++ b/client/src/pages/ImageEditor/Controls/Controls.js
@@ -30,6 +30,7 @@ const Controls = (props) => {
props.onChangeColorCount(e.target.value, props.img)}
+ className={styles.colorCountInput}
placeholder="Max number of colors" />
{(props.colorCount > 1 && props.colorCount < 51) ?
null : Value must be between 2 and 50
}
@@ -49,7 +50,7 @@ const Controls = (props) => {
@@ -83,7 +84,7 @@ const Controls = (props) => {
}
+ onClick={props.onEnablePaint}>Continue
);
};
@@ -107,6 +108,7 @@ const mapDispatchToProps = dispatch => {
onChangeBrightness: (brightness) => dispatch(actions.updateBrightness(brightness)),
onChangeSaturation: (saturation) => dispatch(actions.updateSaturation(saturation)),
onChangeColorCount: (colorCount, image) => dispatch(actions.updateColorCount(colorCount, image)),
+ onEnablePaint: () => dispatch(actions.enablePaint())
};
};
diff --git a/client/src/pages/ImageEditor/Controls/Controls.module.css b/client/src/pages/ImageEditor/Controls/Controls.module.css
index 7c67d61..b4a9c82 100644
--- a/client/src/pages/ImageEditor/Controls/Controls.module.css
+++ b/client/src/pages/ImageEditor/Controls/Controls.module.css
@@ -1,6 +1,6 @@
.controls {
- padding-left: 40px;
- padding-right: 40px;
+ padding-left: 30px;
+ padding-right: 30px;
}
.controls img {
@@ -38,6 +38,10 @@
padding-top: 4px;
}
+.colorCountInput {
+ margin-top: 10px;
+}
+
@media only screen and (max-width: 768px) {
.controls {
padding-left: 30px;
diff --git a/client/src/pages/ImageEditor/FileMenu/FileMenu.js b/client/src/pages/ImageEditor/FileMenu/FileMenu.js
new file mode 100644
index 0000000..755a8d2
--- /dev/null
+++ b/client/src/pages/ImageEditor/FileMenu/FileMenu.js
@@ -0,0 +1,75 @@
+import React, { useState } from 'react';
+import { connect } from 'react-redux';
+import { FiHome, FiRefreshCcw, FiSave } from 'react-icons/fi';
+
+import * as actions from '../../../store/actions';
+import styles from './FileMenu.module.css';
+import Button from '../../../components/Button/Button';
+import Modal from '../../../components/Modal/Modal';
+
+function FileMenu(props) {
+ const [showResetModal, setShowResetModal] = useState(false);
+ const [showHomeModal, setShowHomeModal] = useState(false);
+
+ const handleOpenResetModal = () => setShowResetModal(true);
+ const handleCloseResetModal = () => setShowResetModal(false);
+ const handleOpenHomeModal = () => setShowHomeModal(true);
+ const handleCloseHomeModal = () => setShowHomeModal(false);
+
+ const handleReset = () => {
+ // Reset by re-uploading the image
+ props.onLoadImage(props.img);
+ props.onLoadImageSuccess();
+ props.resetCanvas();
+ setShowResetModal(false);
+ };
+
+ const renderResetModal = () => {
+ return (
+
+ You will lose all the changes you made.
+
+ )
+ };
+
+ const handleReturnHome = () => props.history.push("/");
+
+ const renderHomeModal = () => {
+ return (
+
+ You will lose all the changes you made.
+
+ )
+ };
+
+ return (
+
+
+
+
+ {showResetModal && renderResetModal()}
+ {showHomeModal && renderHomeModal()}
+
+ );
+}
+
+const mapStateToProps = state => {
+ return {
+ img: state.image
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onLoadImage: (img) => dispatch(actions.loadImage(img)),
+ onLoadImageSuccess: () => dispatch(actions.loadImageSuccess())
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(FileMenu);
\ No newline at end of file
diff --git a/client/src/pages/ImageEditor/FileMenu/FileMenu.module.css b/client/src/pages/ImageEditor/FileMenu/FileMenu.module.css
new file mode 100644
index 0000000..5c5968e
--- /dev/null
+++ b/client/src/pages/ImageEditor/FileMenu/FileMenu.module.css
@@ -0,0 +1,5 @@
+.container {
+ display: flex;
+ justify-content: space-between;
+ padding: 0 30px 30px 30px;
+}
\ No newline at end of file
diff --git a/client/src/pages/ImageEditor/ImageEditor.js b/client/src/pages/ImageEditor/ImageEditor.js
index 44e9560..34bbea0 100644
--- a/client/src/pages/ImageEditor/ImageEditor.js
+++ b/client/src/pages/ImageEditor/ImageEditor.js
@@ -1,25 +1,49 @@
import React, { Component } from 'react';
-import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import styles from './ImageEditor.module.css';
-
+import FileMenu from './FileMenu/FileMenu';
import Controls from './Controls/Controls';
import Canvas from './Canvas/Canvas';
import Loading from '../../components/Loading/Loading';
+import PaintTools from './PaintTools/PaintTools';
class ImageEditor extends Component {
+ componentDidMount() {
+ this.validateImage()
+ }
+
+ componentDidUpdate() {
+ this.validateImage()
+ }
+
+ validateImage = () => {
+ // Redirect to home page if image does not have source
+ if (this.props.img.src === null || this.props.img.src === "") {
+ this.props.history.push("/")
+ }
+ }
+
render() {
return (
-
pixel artify
-
this.exportImage()} />
+
+
+ pixel artify
+
+
+ this.exportImage()}
+ resetCanvas={() => this.resetCanvas()}
+ history={this.props.history}/>
+ {this.props.isPaintEnabled ? : }
-
Preview
-
);
@@ -28,7 +52,9 @@ class ImageEditor extends Component {
const mapStateToProps = state => {
return {
- loading: state.loading
+ loading: state.loading,
+ img: state.image,
+ isPaintEnabled: state.isPaintEnabled
};
};
diff --git a/client/src/pages/ImageEditor/ImageEditor.module.css b/client/src/pages/ImageEditor/ImageEditor.module.css
index 1f416c1..017f645 100644
--- a/client/src/pages/ImageEditor/ImageEditor.module.css
+++ b/client/src/pages/ImageEditor/ImageEditor.module.css
@@ -1,11 +1,12 @@
.sidebar {
height: 100%;
- width: 340px;
+ width: 320px;
position: fixed;
top: 0;
left: 0;
background-color: #333;
- overflow-x: hidden;
+ overflow-y: scroll;
+ z-index: 10;
}
.sidebar:after {
@@ -19,31 +20,35 @@
color: white;
text-decoration: none;
text-align: center;
- margin-bottom: 40px;
- padding-top: 15px;
- padding-bottom: 17px;
+ margin-bottom: 20px;
+ padding-top: 10px;
+ padding-bottom: 12px;
border-bottom: 1px solid #555;
}
+.logoLeft {
+ width: 20px;
+ margin-right: 10px;
+}
+
+.logoRight {
+ width: 20px;
+ margin-left: 8px;
+}
+
.content {
- height: 100vh;
- margin-left: 340px;
- position: relative;
+ min-height: 100vh;
+ margin-left: 320px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
background: #444;
}
.content canvas {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
border: 1px dashed #666;
}
-.previewLabel {
- display: none;
-}
-
@media only screen and (max-width: 700px) {
.sidebar {
height: auto;
@@ -53,25 +58,11 @@
}
.content {
+ min-height: 300px;
height: 300px;
width: 100%;
margin-left: 0;
position: fixed;
bottom: 0;
}
-
- .content canvas {
- position: static;
- transform: none;
- display: block;
- margin: 20px auto;
- max-height: 200px;
- }
-
- .previewLabel {
- display: block;
- color: white;
- font-weight: bold;
- margin: 20px 30px;
- }
}
diff --git a/client/src/pages/ImageEditor/PaintTools/PaintTools.js b/client/src/pages/ImageEditor/PaintTools/PaintTools.js
new file mode 100644
index 0000000..65435ca
--- /dev/null
+++ b/client/src/pages/ImageEditor/PaintTools/PaintTools.js
@@ -0,0 +1,86 @@
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { SketchPicker } from 'react-color';
+import { CgColorPicker, CgErase } from "react-icons/cg";
+import { IoIosBrush } from "react-icons/io";
+
+import * as actions from '../../../store/actions';
+import styles from './PaintTools.module.css';
+import { TOOL_TYPES } from '../../../constants/constants';
+import Button from '../../../components/Button/Button';
+import SliderControl from '../../../components/SliderControl/SliderControl';
+
+class PaintTools extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ handleChangeColor = (color) => {
+ this.props.onChangeColor(color.hex);
+ };
+
+ renderToolButton = (toolType, icon) => {
+ const buttonStyle = (this.props.toolType === toolType) ? styles.selected : null;
+ return (
+
+ );
+ };
+
+ render() {
+ const pickerStyles = {
+ picker: {
+ padding: '10px 10px 0',
+ boxSizing: 'initial',
+ background: '#ddd',
+ borderRadius: '4px',
+ }
+ }
+ return (
+
+
+
Paint
+
+ {this.renderToolButton(TOOL_TYPES.PAINT, )}
+ {this.renderToolButton(TOOL_TYPES.COLOR_PICK, )}
+ {this.renderToolButton(TOOL_TYPES.ERASE, )}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ color: state.paintColor,
+ toolType: state.toolType,
+ brushSize: state.brushSize,
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ onChangeColor: (color) => dispatch(actions.updatePaintColor(color)),
+ onChangeToolType: (toolType) => dispatch(actions.updateToolType(toolType)),
+ onChangeBrushSize: (brushSize) => dispatch(actions.updateBrushSize(brushSize)),
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(PaintTools);
\ No newline at end of file
diff --git a/client/src/pages/ImageEditor/PaintTools/PaintTools.module.css b/client/src/pages/ImageEditor/PaintTools/PaintTools.module.css
new file mode 100644
index 0000000..a55f057
--- /dev/null
+++ b/client/src/pages/ImageEditor/PaintTools/PaintTools.module.css
@@ -0,0 +1,40 @@
+.container {
+ margin: 0 30px;
+}
+
+.toolsContainer {
+ display: flex;
+ justify-content: space-between;
+}
+
+.toolsLabel {
+ padding: 5px 0px;
+ color: white;
+ font-weight: bold;
+}
+
+.tools button {
+ border-radius: 4px;
+ background: none;
+ padding: 4px 8px;
+ border: none;
+ color: white;
+}
+
+.tools button:hover {
+ background: grey;
+}
+
+.tools .selected, .tools .selected:hover {
+ background: #ef6461;
+}
+
+.tools button svg {
+ margin-right: 0;
+}
+
+.pickerContainer {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 40px;
+}
\ No newline at end of file
diff --git a/client/src/store/actionTypes.js b/client/src/store/actionTypes.js
index 20fe9d0..8566e10 100644
--- a/client/src/store/actionTypes.js
+++ b/client/src/store/actionTypes.js
@@ -1,7 +1,11 @@
export const LOAD_IMAGE = 'LOAD_IMAGE';
export const LOAD_IMAGE_SUCCESS = 'LOAD_IMAGE_SUCCESS';
export const UPDATE_PIXEL_SIZE = 'UPDATE_PIXEL_SIZE';
+export const UPDATE_BRUSH_SIZE = 'UPDATE_BRUSH_SIZE';
export const UPDATE_CONTRAST = 'UPDATE_CONTRAST';
export const UPDATE_BRIGHTNESS = 'UPDATE_BRIGHTNESS';
export const UPDATE_SATURATION = 'UPDATE_SATURATION';
-export const UPDATE_COLOR_COUNT = 'UPDATE_COLOR_COUNT';
\ No newline at end of file
+export const UPDATE_COLOR_COUNT = 'UPDATE_COLOR_COUNT';
+export const ENABLE_PAINT = 'ENABLE_PAINT';
+export const UPDATE_PAINT_COLOR = 'UPDATE_PAINT_COLOR';
+export const UPDATE_TOOL_TYPE = 'UPDATE_TOOL_TYPE';
\ No newline at end of file
diff --git a/client/src/store/actions.js b/client/src/store/actions.js
index 4994dce..9b0100b 100644
--- a/client/src/store/actions.js
+++ b/client/src/store/actions.js
@@ -21,6 +21,13 @@ export const updatePixelSize = (pixelSize) => {
};
};
+export const updateBrushSize = (brushSize) => {
+ return {
+ type: actionTypes.UPDATE_BRUSH_SIZE,
+ brushSize: brushSize
+ };
+};
+
export const updateContrast = (contrast) => {
return {
type: actionTypes.UPDATE_CONTRAST,
@@ -43,7 +50,6 @@ export const updateSaturation = (saturation) => {
};
};
-
export const updateColorCount = (colorCount, image) => {
const colorThief = new ColorThief();
let palette = [];
@@ -56,3 +62,23 @@ export const updateColorCount = (colorCount, image) => {
colorPalette: palette
};
};
+
+export const enablePaint = () => {
+ return {
+ type: actionTypes.ENABLE_PAINT
+ };
+}
+
+export const updatePaintColor = (color) => {
+ return {
+ type: actionTypes.UPDATE_PAINT_COLOR,
+ paintColor: color
+ };
+};
+
+export const updateToolType = (toolType) => {
+ return {
+ type: actionTypes.UPDATE_TOOL_TYPE,
+ toolType: toolType
+ };
+};
\ No newline at end of file
diff --git a/client/src/store/reducer.js b/client/src/store/reducer.js
index 6b6423a..c0be7ec 100644
--- a/client/src/store/reducer.js
+++ b/client/src/store/reducer.js
@@ -1,46 +1,90 @@
import * as actionTypes from './actionTypes';
-import { updateObject } from '../utils/utility';
+import { updateObject, createReducer, isDevEnv } from '../utils/utility';
import testImg from '../assets/images/car.png';
+import { TOOL_TYPES } from '../constants/constants';
const createTestImage = () => {
const img = new Image();
- img.src = testImg;
+ if (isDevEnv()) {
+ img.src = testImg;
+ }
return img;
};
const initialState = {
loading: false,
image: createTestImage(),
- pixelSize: 4,
+ isPaintEnabled: false,
+ paintColor: '#000',
+ pixelSize: 6,
+ brushSize: 1,
contrast: 0,
brightness: 0,
saturation: 0,
colorCount: '',
- colorPalette: []
+ colorPalette: [],
+ toolType: TOOL_TYPES.PAINT,
};
-const reducer = (state = initialState, action) => {
- switch (action.type) {
- case actionTypes.LOAD_IMAGE:
- return updateObject(initialState, {image: action.image, loading: true});
- case actionTypes.LOAD_IMAGE_SUCCESS:
- return updateObject(state, {loading: false});
- case actionTypes.UPDATE_PIXEL_SIZE:
- return updateObject(state, {pixelSize: action.pixelSize});
- case actionTypes.UPDATE_CONTRAST:
- return updateObject(state, {contrast: action.contrast});
- case actionTypes.UPDATE_BRIGHTNESS:
- return updateObject(state, {brightness: action.brightness});
- case actionTypes.UPDATE_SATURATION:
- return updateObject(state, {saturation: action.saturation});
- case actionTypes.UPDATE_COLOR_COUNT:
- return updateObject(state, {
- colorCount: action.colorCount,
- colorPalette: action.colorPalette
- });
- default:
- return state;
- }
-};
+function loadImage(state, action) {
+ return updateObject(initialState, {image: action.image, loading: true});
+}
+
+function loadImageSuccess(state, action) {
+ return updateObject(state, {loading: false});
+}
+
+function enablePaint(state, action) {
+ return updateObject(state, {isPaintEnabled: true});
+}
+
+function updatePixelSize(state, action) {
+ return updateObject(state, {pixelSize: +action.pixelSize});
+}
+
+function updateBrushSize(state, action) {
+ return updateObject(state, {brushSize: +action.brushSize});
+}
+
+function updateContrast(state, action) {
+ return updateObject(state, {contrast: action.contrast});
+}
+
+function updateBrightness(state, action) {
+ return updateObject(state, {brightness: action.brightness});
+}
+
+function updateSaturation(state, action) {
+ return updateObject(state, {saturation: action.saturation});
+}
+
+function updateColorCount(state, action) {
+ return updateObject(state, {
+ colorCount: action.colorCount,
+ colorPalette: action.colorPalette
+ });
+}
+
+function updatePaintColor(state, action) {
+ return updateObject(state, {paintColor: action.paintColor});
+}
+
+function updateToolType(state, action) {
+ return updateObject(state, {toolType: action.toolType});
+}
+
+const reducer = createReducer(initialState, {
+ [actionTypes.LOAD_IMAGE]: loadImage,
+ [actionTypes.LOAD_IMAGE_SUCCESS]: loadImageSuccess,
+ [actionTypes.ENABLE_PAINT]: enablePaint,
+ [actionTypes.UPDATE_PIXEL_SIZE]: updatePixelSize,
+ [actionTypes.UPDATE_BRUSH_SIZE]: updateBrushSize,
+ [actionTypes.UPDATE_CONTRAST]: updateContrast,
+ [actionTypes.UPDATE_BRIGHTNESS]: updateBrightness,
+ [actionTypes.UPDATE_SATURATION]: updateSaturation,
+ [actionTypes.UPDATE_COLOR_COUNT]: updateColorCount,
+ [actionTypes.UPDATE_PAINT_COLOR]: updatePaintColor,
+ [actionTypes.UPDATE_TOOL_TYPE]: updateToolType,
+});
export default reducer;
\ No newline at end of file
diff --git a/client/src/utils/colorDifference.js b/client/src/utils/colorDifference.js
index 39db52c..99bd597 100644
--- a/client/src/utils/colorDifference.js
+++ b/client/src/utils/colorDifference.js
@@ -1,3 +1,19 @@
+// From a given list of colors, return the color that is the most
+// similar to the given target color.
+export function getClosestColor(colors, target) {
+ let minDiff = Number.MAX_SAFE_INTEGER;
+ let closest = null;
+ const length = colors.length;
+ for (let i = 0; i < length; i++) {
+ let diff = getColorDifference(target, colors[i]);
+ if (diff <= minDiff) {
+ minDiff = diff;
+ closest = colors[i];
+ }
+ }
+ return closest;
+}
+
export function getColorDifference(rgb1, rgb2) {
const lab1 = convertRgbToLgb(rgb1);
const lab2 = convertRgbToLgb(rgb2);
@@ -10,6 +26,11 @@ export function convertRgbToLgb(rgb) {
return lab;
}
+// https://stackoverflow.com/a/5624139
+export function convertRgbToHex(r, g, b) {
+ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
+}
+
/**
* Convert Standard-RGB to XYZ color space. Adapted from
* http://www.easyrgb.com/en/math.php
@@ -117,4 +138,22 @@ export function deltaE(labA, labB){
var deltaHkhsh = deltaH / (sh);
var i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
return i < 0 ? 0 : Math.sqrt(i);
+}
+
+// Adjust the saturation, brightness, and contrast of an image
+export function adjustImageColors(img, saturation, brightness, contrast) {
+ const s = (saturation / 100) + 1;
+ const b = brightness * 0.75;
+ const c = (contrast / 100) + 1;
+ const intercept = 128 * (1 - c); // line intercept for contrast https://stackoverflow.com/a/37714937
+
+ for (let i = 0; i < img.length; i += 4) {
+ // Calculate grayness for adjusting saturation https://stackoverflow.com/a/34183839
+ const gray = img[i] * 0.3086 + img[i + 1] * 0.6094 + img[i + 2] * 0.0820;
+ const grayVal = gray * (1 - s);
+ // Update pixel values with saturation, brightness, and contrast adjusted
+ img[i] = (img[i] * s + grayVal + b) * c + intercept;
+ img[i + 1] = (img[i + 1] * s + grayVal + b) * c + intercept;
+ img[i + 2] = (img[i + 2] * s + grayVal + b) * c + intercept;
+ }
}
\ No newline at end of file
diff --git a/client/src/utils/utility.js b/client/src/utils/utility.js
index fccf025..05b14a6 100644
--- a/client/src/utils/utility.js
+++ b/client/src/utils/utility.js
@@ -3,4 +3,17 @@ export const updateObject = (oldObject, updatedValues) => {
...oldObject,
...updatedValues
}
-};
\ No newline at end of file
+};
+
+// https://redux.js.org/usage/structuring-reducers/refactoring-reducer-example
+export function createReducer(initialState, handlers) {
+ return function reducer(state = initialState, action) {
+ if (handlers.hasOwnProperty(action.type)) {
+ return handlers[action.type](state, action)
+ } else {
+ return state
+ }
+ }
+}
+
+export const isDevEnv = () => process.env.NODE_ENV === 'development';
\ No newline at end of file
diff --git a/client/yarn.lock b/client/yarn.lock
index 337ef09..29fdc72 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1095,6 +1095,13 @@
dependencies:
regenerator-runtime "^0.13.2"
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
+ version "7.18.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.0.tgz#6d77142a19cb6088f0af662af1ada37a604d34ae"
+ integrity sha512-YMQvx/6nKEaucl0MY56mwIG483xk8SDNdlUwb2Ts6FUpr7fm85DxEmsY18LXBNhcTz6tO6JwZV8w1W06v8UKeg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2":
version "7.2.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
@@ -1205,6 +1212,11 @@
dependencies:
"@hapi/hoek" "6.x.x"
+"@icons/material@^0.2.4":
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
+ integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
+
"@jest/console@^24.7.1":
version "24.7.1"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.7.1.tgz#32a9e42535a97aedfe037e725bd67e954b459545"
@@ -1502,6 +1514,14 @@
dependencies:
"@babel/types" "^7.3.0"
+"@types/hoist-non-react-statics@^3.3.0":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@@ -1527,11 +1547,40 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.10.5.tgz#fbaca34086bdc118011e1f05c47688d432f2d571"
integrity sha512-DuIRlQbX4K+d5I+GMnv+UfnGh+ist0RdlvOp+JZ7ePJ6KQONCFQv/gKYSU1ZzbVdFSUCKZOltjmpFAGGv5MdYA==
+"@types/prop-types@*":
+ version "15.7.5"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
+ integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
+
"@types/q@^1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18"
integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==
+"@types/react-redux@^7.1.20":
+ version "7.1.24"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
+ integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
+
+"@types/react@*":
+ version "18.0.9"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878"
+ integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
+"@types/scheduler@*":
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
+ integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -3344,6 +3393,11 @@ cssstyle@^1.1.1:
dependencies:
cssom "0.3.x"
+csstype@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
+ integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
+
cyclist@~0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@@ -4258,6 +4312,11 @@ file-loader@3.0.1:
loader-utils "^1.0.2"
schema-utils "^1.0.0"
+file-saver@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+ integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
filesize@3.6.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
@@ -4612,6 +4671,11 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
+glur@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689"
+ integrity sha1-8g6jbbEDv8KSNDkh8fkeg8NGdok=
+
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
@@ -4776,6 +4840,18 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+history@^4.9.0:
+ version "4.10.1"
+ resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
+ integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+ loose-envify "^1.2.0"
+ resolve-pathname "^3.0.0"
+ tiny-invariant "^1.0.2"
+ tiny-warning "^1.0.0"
+ value-equal "^1.0.1"
+
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -4785,6 +4861,13 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
hosted-git-info@^2.1.4:
version "2.7.1"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
@@ -5400,6 +5483,11 @@ is-wsl@^1.1.0:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+ integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -6188,6 +6276,11 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
+lodash-es@^4.17.15:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+ integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
lodash._reinterpolate@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -6243,12 +6336,17 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+lodash@^4.0.1, lodash@^4.17.15:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
loglevel@^1.4.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=
-loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -6313,6 +6411,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
+material-colors@^1.2.1:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
+ integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
+
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -6434,6 +6537,14 @@ mimic-fn@^1.0.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+mini-create-react-context@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e"
+ integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==
+ dependencies:
+ "@babel/runtime" "^7.12.1"
+ tiny-warning "^1.0.3"
+
mini-css-extract-plugin@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
@@ -6564,6 +6675,14 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1"
thunky "^1.0.2"
+multimath@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/multimath/-/multimath-2.0.0.tgz#0d37acf67c328f30e3d8c6b0d3209e6082710302"
+ integrity sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==
+ dependencies:
+ glur "^1.1.2"
+ object-assign "^4.1.1"
+
mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
@@ -7187,6 +7306,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+path-to-regexp@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+ integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+ dependencies:
+ isarray "0.0.1"
+
path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@@ -7217,6 +7343,16 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+pica@^9.0.1:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/pica/-/pica-9.0.1.tgz#9ba5a5e81fc09dca9800abef9fb8388434b18b2f"
+ integrity sha512-v0U4vY6Z3ztz9b4jBIhCD3WYoecGXCQeCsYep+sXRefViL+mVVoTL+wqzdPeE+GpBFsRUtQZb6dltvAt2UkMtQ==
+ dependencies:
+ glur "^1.1.2"
+ multimath "^2.0.0"
+ object-assign "^4.1.1"
+ webworkify "^1.5.0"
+
pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -8019,6 +8155,15 @@ prompts@^2.0.1:
kleur "^3.0.2"
sisteransi "^1.0.0"
+prop-types@^15.5.10, prop-types@^15.7.2:
+ version "15.8.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
prop-types@^15.6.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -8189,6 +8334,19 @@ react-app-polyfill@^1.0.0:
regenerator-runtime "0.13.2"
whatwg-fetch "3.0.0"
+react-color@^2.19.3:
+ version "2.19.3"
+ resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"
+ integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==
+ dependencies:
+ "@icons/material" "^0.2.4"
+ lodash "^4.17.15"
+ lodash-es "^4.17.15"
+ material-colors "^1.2.1"
+ prop-types "^15.5.10"
+ reactcss "^1.2.0"
+ tinycolor2 "^1.4.1"
+
react-dev-utils@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.0.0.tgz#356d95db442441c5d4748e0e49f4fd1e71aecbbd"
@@ -8220,21 +8378,31 @@ react-dev-utils@^9.0.0:
strip-ansi "5.2.0"
text-table "0.2.0"
-react-dom@16.8.6:
- version "16.8.6"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
- integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==
+react-dom@^16.8.6:
+ version "16.14.0"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
+ integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
- scheduler "^0.13.6"
+ scheduler "^0.19.1"
react-error-overlay@^5.1.5:
version "5.1.5"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.5.tgz#884530fd055476c764eaa8ab13b8ecf1f57bbf2c"
integrity sha512-O9JRum1Zq/qCPFH5qVEvDDrVun8Jv9vbHtZXCR1EuRj9sKg1xJTlHxBzU6AkCzpvxRLuiY4OKImy3cDLQ+UTdg==
+react-icons@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca"
+ integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==
+
+react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
react-is@^16.8.1:
version "16.8.4"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
@@ -8245,6 +8413,52 @@ react-is@^16.8.4:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
+react-is@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
+react-redux@^7.0.3:
+ version "7.2.8"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
+ integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
+ dependencies:
+ "@babel/runtime" "^7.15.4"
+ "@types/react-redux" "^7.1.20"
+ hoist-non-react-statics "^3.3.2"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^17.0.2"
+
+react-router-dom@^5.0.0:
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.3.tgz#8779fc28e6691d07afcaf98406d3812fe6f11199"
+ integrity sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng==
+ dependencies:
+ "@babel/runtime" "^7.12.13"
+ history "^4.9.0"
+ loose-envify "^1.3.1"
+ prop-types "^15.6.2"
+ react-router "5.3.3"
+ tiny-invariant "^1.0.2"
+ tiny-warning "^1.0.0"
+
+react-router@5.3.3:
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.3.tgz#8e3841f4089e728cf82a429d92cdcaa5e4a3a288"
+ integrity sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==
+ dependencies:
+ "@babel/runtime" "^7.12.13"
+ history "^4.9.0"
+ hoist-non-react-statics "^3.1.0"
+ loose-envify "^1.3.1"
+ mini-create-react-context "^0.4.0"
+ path-to-regexp "^1.7.0"
+ prop-types "^15.6.2"
+ react-is "^16.6.0"
+ tiny-invariant "^1.0.2"
+ tiny-warning "^1.0.0"
+
react-scripts@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.0.0.tgz#a715613ef3eace025907b409cec8505096e0233e"
@@ -8303,15 +8517,21 @@ react-scripts@3.0.0:
optionalDependencies:
fsevents "2.0.6"
-react@16.8.6:
- version "16.8.6"
- resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"
- integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==
+react@^16.8.6:
+ version "16.14.0"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
+ integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
- scheduler "^0.13.6"
+
+reactcss@^1.2.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
+ integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
+ dependencies:
+ lodash "^4.0.1"
read-pkg-up@^2.0.0:
version "2.0.0"
@@ -8392,6 +8612,13 @@ recursive-readdir@2.2.2:
dependencies:
minimatch "3.0.4"
+redux@^4.0.0, redux@^4.0.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
+ integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+
regenerate-unicode-properties@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.1.tgz#58a4a74e736380a7ab3c5f7e03f303a941b31289"
@@ -8421,6 +8648,11 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+regenerator-runtime@^0.13.4:
+ version "0.13.9"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
+ integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
+
regenerator-transform@^0.13.4:
version "0.13.4"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb"
@@ -8616,6 +8848,11 @@ resolve-from@^4.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+resolve-pathname@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
+ integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
+
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -8753,10 +8990,10 @@ saxes@^3.1.9:
dependencies:
xmlchars "^1.3.1"
-scheduler@^0.13.6:
- version "0.13.6"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889"
- integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==
+scheduler@^0.19.1:
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
+ integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -9483,6 +9720,21 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+tiny-invariant@^1.0.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
+ integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
+
+tiny-warning@^1.0.0, tiny-warning@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+ integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
+tinycolor2@^1.4.1:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+ integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -9836,6 +10088,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
+value-equal@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
+ integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
+
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -10042,6 +10299,11 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+webworkify@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"
+ integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==
+
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"