diff --git a/package.json b/package.json index ebb301b1..ec18eca7 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ "husky": "^4.3.0", "next-qrcode": "^2.4.0", "react": "^16.13.1", + "react-color": "2.17.2", "react-csv": "^2.2.1", "react-dom": "^16.13.1", "react-markdown": "^4.3.1", "react-scripts": "3.4.1", "react-select": "^3.1.0", + "react-sketch-canvas": "^6.2.0", "react-spinner": "^0.2.7", "react-spinners": "^0.9.0", "styled-components": "^5.1.1", diff --git a/src/assets/icons/colorpicker.svg b/src/assets/icons/colorpicker.svg new file mode 100644 index 00000000..ea09b4c8 --- /dev/null +++ b/src/assets/icons/colorpicker.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/redo.svg b/src/assets/icons/redo.svg new file mode 100644 index 00000000..d199a660 --- /dev/null +++ b/src/assets/icons/redo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/icons/trashcan.svg b/src/assets/icons/trashcan.svg new file mode 100644 index 00000000..1c5dceab --- /dev/null +++ b/src/assets/icons/trashcan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/undo.svg b/src/assets/icons/undo.svg new file mode 100644 index 00000000..d711d493 --- /dev/null +++ b/src/assets/icons/undo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/ApplicationForm/Skills.js b/src/components/ApplicationForm/Skills.js index 9da4d9c7..1b0e3334 100644 --- a/src/components/ApplicationForm/Skills.js +++ b/src/components/ApplicationForm/Skills.js @@ -6,6 +6,7 @@ import { Select } from '../Input' import { FormSpacing, SubHeading } from './' import { CONTRIBUTION_ROLE_OPTIONS, copyText } from '../../utility/Constants' import styled from 'styled-components' +import SketchCanvas from '../Input/SketchCanvas' const QuestionForm = styled.form` display: flex; @@ -275,6 +276,24 @@ export default ({ refs, errors, formInputs, onChange, role, handleResume }) => { customRef={refs['longAnswers2Ref']} /> + + question 21 + Draw your favourite character! + + onChange({ + longAnswers3: val, + }) + } + customRef={refs['longAnswers3Ref']} + exportAsBase64 + /> + ) diff --git a/src/components/Input/SketchCanvas.js b/src/components/Input/SketchCanvas.js new file mode 100644 index 00000000..6c605354 --- /dev/null +++ b/src/components/Input/SketchCanvas.js @@ -0,0 +1,235 @@ +/* eslint-disable import/no-anonymous-default-export */ +import React, { useEffect, useRef, useState } from 'react' +import { ReactSketchCanvas } from 'react-sketch-canvas' +import { SketchPicker } from 'react-color' +import trashcanIcon from '../../assets/icons/trashcan.svg' +import colorpickerIcon from '../../assets/icons/colorpicker.svg' +import undoIcon from '../../assets/icons/undo.svg' +import redoIcon from '../../assets/icons/redo.svg' +import { mergeRefs, useDebounce } from '../../utility/utilities' +import styled from 'styled-components' + +const CanvasContainer = styled.div` + background: #f3f3f3; + display: flex; + flex-direction: column; + position: relative; +` + +const ErrorMessage = styled.p` + position: absolute; + top: 0px; + left: 0px; + padding: 5px; + padding-left: 8px; + padding-right: 8px; + margin: 0px; + background: #ffaaaa; + color: red; + z-index: 10; + box-sizing: border-box; +` + +const Stroke4Button = styled.div` + background: #333333; + border-radius: 999px; + cursor: pointer; + width: 8px; + height: 8px; +` + +const Stroke6Button = styled.div` + background: #333333; + border-radius: 999px; + cursor: pointer; + width: 12px; + height: 12px; +` + +const Stroke8Button = styled.div` + background: #333333; + border-radius: 999px; + cursor: pointer; + width: 16px; + height: 16px; +` + +const CanvasIcon = styled.img` + width: 20px; + height: 25px; + cursor: pointer; +` + +const HistoryIcons = styled.div` + display: flex; + flex-direction: row; + gap: 5px; + align-items: center; +` + +const ColorPickerWrapper = styled.div` + position: absolute; + z-index: 100; + top: 0; + right: 0; +` + +const LeftIcons = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; +` + +const ToolBar = styled.div` + display: flex; + flex-direction: row; + padding: 3px; + padding-left: 7px; + padding-right: 7px; + justify-content: space-between; + user-select: none; +` +/** + * @param width number, width of the entire component + * @param height number, height of the entire component + * @param invalid boolean, indicates dissatisfaction with current value + * @param errorMsg string, indicates some error + * @param onChange (result: string) => void, triggers with result (SVG or Base64) on change. Has debouncing. + * @param customRef a ref directly to the canvas component + * @param exportAsBase64 boolean, onChange() will return a base64 string instead of svg if true + */ +export default ({ width, height, invalid, errorMsg, onChange, customRef, exportAsBase64 }) => { + const [strokeWidth, setStrokeWidth] = useState(4) + const [strokeColor, setStrokeColor] = useState('black') + const [showPicker, setShowPicker] = useState(false) + const canvasRef = useRef() + + const loadedRef = useRef(false) + + useEffect(() => { + loadedRef.current = true + return () => { + loadedRef.current = false + } + }, []) + + const debounceUpdate = useDebounce(async () => { + if (!loadedRef.current) return + + if (exportAsBase64) { + const newBase64 = await canvasRef.current.exportImage('jpeg') + onChange(newBase64) + } else { + const newSVG = await canvasRef.current.exportSvg() + onChange(newSVG) + } + }, 500) + + const handleChange = e => { + if (!onChange) return + + debounceUpdate() + } + + const handleClear = () => { + if (canvasRef.current) canvasRef.current.clearCanvas() + } + + const handleUndo = () => { + if (canvasRef.current) canvasRef.current.undo() + } + + const handleRedo = () => { + if (canvasRef.current) canvasRef.current.redo() + } + + return ( + { + setShowPicker(false) + }} + > + {errorMsg && {errorMsg}} + + + + + + { + e.stopPropagation() + setShowPicker(!showPicker) + }} + /> + { + setStrokeWidth(4) + }} + /> + { + setStrokeWidth(6) + }} + /> + { + setStrokeWidth(8) + }} + /> + + + + + + + + + + {showPicker && ( + { + e.stopPropagation() + }} + > + { + setStrokeColor(e.hex) + }} + /> + + )} + + + ) +} diff --git a/src/utility/utilities.js b/src/utility/utilities.js index fc8a7646..b5af2cb9 100644 --- a/src/utility/utilities.js +++ b/src/utility/utilities.js @@ -144,3 +144,11 @@ export const cutString = (string, maxLength) => { } return `${string.substring(0, cut)}...` } + +export const mergeRefs = (...refs) => { + return node => { + for (const ref of refs) { + ref.current = node + } + } +} diff --git a/yarn.lock b/yarn.lock index 0db73742..44fb5282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1303,6 +1303,11 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@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", "@jest/console@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" @@ -6406,6 +6411,11 @@ lodash.uniq@^4.5.0: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" +lodash@^4.0.1: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + lodash@^4.17.19: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" @@ -6479,6 +6489,11 @@ markdown-escapes@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" +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" @@ -8133,6 +8148,15 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" +prop-types@^15.5.10: + 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.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -8307,6 +8331,18 @@ react-app-polyfill@^1.0.6: regenerator-runtime "^0.13.3" whatwg-fetch "^3.0.0" +react-color@2.17.2: + version "2.17.2" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.17.2.tgz#ef002cafd5ae8daf3d374b85bb44ac9d36785c0a" + integrity sha512-TpT/D3wEQmyCOcIXrUgKOvEXpJtMylEx5yzw0laGP30mfC5yO4CoUsiYvfPiUbbkFjMGurPJjkKOkkOfBBIjFQ== + dependencies: + "@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-csv@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-csv/-/react-csv-2.2.1.tgz#2ec723dd3ce8069269bffb3a95d968302fc87740" @@ -8361,7 +8397,7 @@ react-input-autosize@^2.2.2: dependencies: prop-types "^15.5.8" -react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -8451,6 +8487,11 @@ react-select@^3.1.0: react-input-autosize "^2.2.2" react-transition-group "^4.3.0" +react-sketch-canvas@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/react-sketch-canvas/-/react-sketch-canvas-6.2.0.tgz#4aa1f0b85012f37b8b2ae746a412ab9424badfa9" + integrity sha512-Z56RQN0viV9W3Y+DpZfQMi2lf3pO+UB4l+cMwnx7j7pQ9WHkQwN+qZKVFGANgpjXQMyY42NODF/DXjuKPCMf1w== + react-spinner@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/react-spinner/-/react-spinner-0.2.7.tgz#ea3ca3375dd7a54edbb5cc01d17496a2e2fc14db" @@ -8481,6 +8522,13 @@ react@^16.13.1: object-assign "^4.1.1" prop-types "^15.6.2" +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" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -9694,6 +9742,11 @@ timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" +tinycolor2@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" + integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"