From 4dfadade12123aac7554e14c01e74a19eacaa816 Mon Sep 17 00:00:00 2001 From: Haley Date: Tue, 11 Jun 2019 17:07:21 -0700 Subject: [PATCH 01/16] made color picker compatible with Safari --- package-lock.json | 36 +++++++++++++++++++++++ package.json | 1 + src/components/ColorPicker.js | 23 +++++++++++++++ src/components/GenerateGifForm.css | 12 ++++++++ src/components/GenerateGifForm.js | 46 +++++++++++++++++++++++------- 5 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 src/components/ColorPicker.js diff --git a/package-lock.json b/package-lock.json index 95565ff..60564fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -851,6 +851,11 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -8510,6 +8515,11 @@ "object-visit": "^1.0.0" } }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" + }, "math-random": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", @@ -11289,6 +11299,19 @@ } } }, + "react-color": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.17.3.tgz", + "integrity": "sha512-1dtO8LqAVotPIChlmo6kLtFS1FP89ll8/OiA8EcFRDR+ntcK+0ukJgByuIQHRtzvigf26dV5HklnxDIvhON9VQ==", + "requires": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.11", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + } + }, "react-dev-utils": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-7.0.0.tgz", @@ -11521,6 +11544,14 @@ "dom-testing-library": "^3.13.1" } }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "requires": { + "lodash": "^4.0.1" + } + }, "read-pkg": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", @@ -13615,6 +13646,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", diff --git a/package.json b/package.json index 212831d..8da80a6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "classnames": "^2.2.6", "downloadjs": "^1.4.7", "react": "^16.7.0", + "react-color": "^2.17.3", "react-dom": "^16.7.0", "react-redux": "^6.0.0", "react-scripts": "2.1.2", diff --git a/src/components/ColorPicker.js b/src/components/ColorPicker.js new file mode 100644 index 0000000..c6d3bd4 --- /dev/null +++ b/src/components/ColorPicker.js @@ -0,0 +1,23 @@ +import React, { Component } from 'react'; +import { SketchPicker } from 'react-color'; + +class ColorPicker extends Component { + handleInputUpdate = color => { + console.log(color); + this.props.updateTextColor(color.hex); + }; + + render() { + return ( +
+ +
+ ); + } +} + +export default ColorPicker; diff --git a/src/components/GenerateGifForm.css b/src/components/GenerateGifForm.css index d3bf4f1..b2711e6 100644 --- a/src/components/GenerateGifForm.css +++ b/src/components/GenerateGifForm.css @@ -29,6 +29,18 @@ animation: fade-in 0.5s; } +.colorPicker { + display: block; + width: 210px; + margin: 15px auto 0; + color: rgb(138, 138, 138); + font-size: 1em; + padding: 1px; + border-radius: 5px; + outline: none !important; + text-align: center; +} + .GenerateGifForm-button:hover { background: #484848; } diff --git a/src/components/GenerateGifForm.js b/src/components/GenerateGifForm.js index 5def4cd..eb1f6a4 100644 --- a/src/components/GenerateGifForm.js +++ b/src/components/GenerateGifForm.js @@ -1,11 +1,17 @@ import React, { Component } from 'react'; import './GenerateGifForm.css'; +import ColorPicker from './ColorPicker'; class GenerateGifForm extends Component { constructor(props) { super(props); + this.state = { + color: false + }; this.handleInputUpdate = this.handleInputUpdate.bind(this); this.handleSubmit = this.handleSubmit.bind(this); + this.renderColorPicker = this.renderColorPicker.bind(this); + this.removeColorPicker = this.removeColorPicker.bind(this); } handleSubmit(evt) { @@ -19,15 +25,34 @@ class GenerateGifForm extends Component { if (evt.target.name === 'caption') { this.props.updateText(evt.target.value); } - if (evt.target.name === 'fontColor') { - this.props.updateTextColor(evt.target.value); - } + if (evt.target.name === 'name') { this.props.updateGIFFileName(evt.target.value); } } + renderColorPicker() { + this.setState(state => ({ + color: !state.color + })); + } + + removeColorPicker() { + this.setState({ + color: false + }); + } + render() { + console.log(this.state.color); + let colorPicker = ( + + ); + let colorPickerBg = { backgroundColor: this.props.fontColor }; return (
- +
+

pick a color

+
+ {this.state.color ? colorPicker : null} + + ); + }, this)} + + ) : ( +
+ No Saved Graphs +
+ ); + + return ( +
+
+
Name
+ +
+ +
+
+
Saved Graphs
+
{savedList}
+
+ ); + } +} + +export default Folder; diff --git a/src/components/GenerateGifForm.js b/src/components/GenerateGifForm.js index 0d82a94..a617244 100644 --- a/src/components/GenerateGifForm.js +++ b/src/components/GenerateGifForm.js @@ -25,12 +25,20 @@ class GenerateGifForm extends Component { } handleInputUpdate(evt) { - if (evt.target.name === 'caption') { - this.props.updateText(evt.target.value); - } + switch (evt.target.name) { + case 'caption': + this.props.updateText(evt.target.value); + break; + case 'fontColor': + this.props.updateTextColor(evt.target.value); + break; + + case 'name': + this.props.updateGIFFileName(evt.target.value); + break; - if (evt.target.name === 'name') { - this.props.updateGIFFileName(evt.target.value); + default: + break; } if (evt.target.name === 'placement') { diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js index e5bc1aa..fdd3d20 100644 --- a/src/components/Sidebar.js +++ b/src/components/Sidebar.js @@ -13,6 +13,7 @@ class Sidebar extends Component { this.handleToggleSettings = this.handleToggleSettings.bind(this); this.handleRequestFrame = this.handleRequestFrame.bind(this); this.handleDownload = this.handleDownload.bind(this); + this.handleToggleFiles = this.handleToggleFiles.bind(this); } handleTogglePreview() { @@ -45,6 +46,11 @@ class Sidebar extends Component { requestFrame(imageOpts); } + handleToggleFiles() { + const { togglePane } = this.props; + togglePane(panes.FILES); + } + render() { const { reset, expandedPane, numFrames, gifData } = this.props; @@ -67,6 +73,12 @@ class Sidebar extends Component { value={numFrames > 99 ? '99+' : numFrames} /> + + ( diff --git a/src/components/icons/folder.svg b/src/components/icons/folder.svg new file mode 100644 index 0000000..95d07ec --- /dev/null +++ b/src/components/icons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/constants/pane-types.js b/src/constants/pane-types.js index 6d32bcb..e489c1c 100644 --- a/src/constants/pane-types.js +++ b/src/constants/pane-types.js @@ -6,7 +6,8 @@ const panes = { PREVIEW: 'PREVIEW', BURST: 'BURST', SETTINGS: 'SETTINGS', - NONE: 'NONE' + NONE: 'NONE', + FILES: 'FILES' }; export default panes; diff --git a/src/containers/FolderContainer.js b/src/containers/FolderContainer.js new file mode 100644 index 0000000..6655f15 --- /dev/null +++ b/src/containers/FolderContainer.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux'; +import Folder from '../components/Folder'; +import { togglePane, loadFramesFromLocal, saveGraph } from '../actions'; +import panes from '../constants/pane-types'; + +const mapStateToProps = (state, ownProps) => { + const { ui, images } = state; + + return { + expanded: ui.expandedPane === panes.FILES, + images + }; +}; + +const FolderContainer = connect( + mapStateToProps, + { togglePane, loadFramesFromLocal, saveGraph } +)(Folder); + +export default FolderContainer; diff --git a/src/lib/calc-helpers.js b/src/lib/calc-helpers.js index 57cda8c..d88b5c8 100644 --- a/src/lib/calc-helpers.js +++ b/src/lib/calc-helpers.js @@ -6,8 +6,9 @@ * and concise. */ -import { calculator } from '../components/App'; +import { calculator } from './calculator'; import { noSuchExpression, notASlider } from './error-messages'; +import { saveGraphToLocal, getGraphFromLocal } from './local-storage-helpers'; /* * The calculator's async screenshot method takes a callback, but we'd prefer to @@ -27,10 +28,14 @@ export const getImageData = opts => */ const getExpByIndex = idx => calculator.getExpressions()[idx - 1]; -// Returns an error message on failure. +/** + * Returns an error message on failure. + * Skips expressions that are not of type 'expression' + */ export const setSliderByIndex = (idx, val) => { const exp = getExpByIndex(idx); if (!exp) return noSuchExpression(idx); + if (exp.type !== 'expression') return notASlider(idx); const { id, latex } = exp; const match = latex.match(/(.+)=/); @@ -39,3 +44,20 @@ export const setSliderByIndex = (idx, val) => { const identifier = match[1]; calculator.setExpression({ id, latex: `${identifier}=${val}` }); }; + +export const saveCurrentGraph = async (name, frames, frameIDs) => { + const graph = calculator.getState(); + const preview = await getImageData({ + width: 160, + height: 160, + targetPixelRatio: 0.25 + }); + + return saveGraphToLocal(graph, name, preview, frames, frameIDs); +}; + +export const loadSavedGraph = dateString => { + const { graph, frames, frameIDs } = getGraphFromLocal(dateString); + calculator.setState(graph); + return { frames, frameIDs }; +}; diff --git a/src/lib/calculator.js b/src/lib/calculator.js new file mode 100644 index 0000000..9be4f4b --- /dev/null +++ b/src/lib/calculator.js @@ -0,0 +1,9 @@ +let calculator; + +function initializeCalculator(desmos, calcContainer, calcOptions) { + calculator = desmos.GraphingCalculator(calcContainer.current, calcOptions); + window.calculator = calculator; + return calculator; +} + +export { calculator, initializeCalculator }; diff --git a/src/lib/error-messages.js b/src/lib/error-messages.js index fdb97f3..eb8a851 100644 --- a/src/lib/error-messages.js +++ b/src/lib/error-messages.js @@ -11,6 +11,9 @@ export const noSuchExpression = idx => export const notASlider = idx => `Looks like expression ${idx} doesn't define a slider.`; +export const badNameInput = errorMessage => + `Invalid name input for saving graph: ${errorMessage}`; + export const gifCreationProblem = () => 'There was a problem creating your GIF. :('; diff --git a/src/lib/input-helpers.js b/src/lib/input-helpers.js index d4f4d31..cc5140e 100644 --- a/src/lib/input-helpers.js +++ b/src/lib/input-helpers.js @@ -35,3 +35,13 @@ export const getSettingsErrors = inputs => { return errors; }; + +export const getSaveGraphErrors = name => { + const errors = {}; + if (!name.length) { + errors.name = 'Name required'; + } else if (name.length > 255) { + errors.name = `Name cannot be longer than 255 characters`; + } + return errors; +}; diff --git a/src/lib/local-storage-helpers.js b/src/lib/local-storage-helpers.js new file mode 100644 index 0000000..b3b0b85 --- /dev/null +++ b/src/lib/local-storage-helpers.js @@ -0,0 +1,43 @@ +/** + * Helper functions for loading and saving graphs to/from local storage + */ + +export const saveGraphToLocal = (graph, name, preview, frames, frameIDs) => { + let savedGraphs = localStorage.getItem('gifsmos-saved-graphs'); + savedGraphs = savedGraphs ? JSON.parse(savedGraphs) : { saved: {} }; + const newGraph = { + name, + graph, + preview, + frames, + frameIDs + }; + let timeStamp = new Date().toLocaleString(); + savedGraphs.saved[timeStamp] = newGraph; + + localStorage.setItem('gifsmos-saved-graphs', JSON.stringify(savedGraphs)); + return [timeStamp, name, preview]; +}; + +export const getGraphFromLocal = dateString => { + let savedGraphs = JSON.parse(localStorage.getItem('gifsmos-saved-graphs')); + return savedGraphs.saved[dateString]; +}; + +export const getSavedGraphsList = () => { + let savedGraphs = JSON.parse(localStorage.getItem('gifsmos-saved-graphs')); + + return savedGraphs + ? Object.entries(savedGraphs.saved).map(([date, obj]) => [ + date, + obj.name, + obj.preview + ]) + : []; +}; + +export const removeGraphFromLocal = dateString => { + let savedGraphs = JSON.parse(localStorage.getItem('gifsmos-saved-graphs')); + delete savedGraphs.saved[dateString]; + localStorage.setItem('gifsmos-saved-graphs', JSON.stringify(savedGraphs)); +}; diff --git a/src/reducers/images.js b/src/reducers/images.js index 47a192f..cecc2a4 100644 --- a/src/reducers/images.js +++ b/src/reducers/images.js @@ -89,9 +89,7 @@ const images = (state = initialState, { type, payload }) => { case UPDATE_GIF_FILENAME: return { ...state, - ...{ - gifFileName: payload.gifFileName - } + gifFileName: payload.gifFileName }; case UPDATE_TEXT_POSITION: diff --git a/src/reducers/images.test.js b/src/reducers/images.test.js index 0ca83b8..873a888 100644 --- a/src/reducers/images.test.js +++ b/src/reducers/images.test.js @@ -13,7 +13,10 @@ const initialState = { frames: {}, frameIDs: [], gifProgress: 0, - gifData: '' + gifData: '', + caption: '', + fontColor: '#000000', + gifFileName: '' }; describe('reducers', () => { diff --git a/src/setupFiles.js b/src/setupFiles.js new file mode 100644 index 0000000..e69de29 From 302be2aad86980376ff280430da6da868d3c2ce0 Mon Sep 17 00:00:00 2001 From: Haley Date: Mon, 24 Jun 2019 11:06:19 -0700 Subject: [PATCH 12/16] holding position state in store when generating gif --- src/actions/index.js | 1 + src/components/GenerateGifForm.js | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 7495984..2d27a86 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -161,6 +161,7 @@ export const clearError = () => ({ type: types.CLEAR_ERROR }); export const reset = () => { clearTimer(); + localStorage.clear(); return { type: types.RESET }; }; diff --git a/src/components/GenerateGifForm.js b/src/components/GenerateGifForm.js index a617244..7694508 100644 --- a/src/components/GenerateGifForm.js +++ b/src/components/GenerateGifForm.js @@ -18,10 +18,6 @@ class GenerateGifForm extends Component { handleSubmit(evt) { evt.preventDefault(); this.props.handleGenerateGIF(); - this.props.updateTextPosition({ - textAlign: 'center', - textBaseline: 'bottom' - }); } handleInputUpdate(evt) { @@ -37,19 +33,22 @@ class GenerateGifForm extends Component { this.props.updateGIFFileName(evt.target.value); break; + case 'placement': + let values = evt.target.value.split('-'); + let positionObj = { + textAlign: values[1], + textBaseline: values[0] + }; + this.props.updateTextPosition(positionObj); + let selectValue = document.getElementById('GenerateGifForm-select'); + selectValue = selectValue.options[selectValue.selectedIndex].value; + localStorage.setItem('selectValue', selectValue); + this.props.updateTextPosition(positionObj); + + break; default: break; } - - if (evt.target.name === 'placement') { - let values = evt.target.value.split('-'); - let positionObj = { - textAlign: values[1], - textBaseline: values[0] - }; - - this.props.updateTextPosition(positionObj); - } } renderColorPicker() { @@ -63,6 +62,7 @@ class GenerateGifForm extends Component { } render() { + let currentValue = localStorage.getItem('selectValue') || 'DEFAULT'; let colorPicker = (