Skip to content
This repository was archived by the owner on Dec 4, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/assets/icons/colorpicker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/redo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/trashcan.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icons/undo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions src/components/ApplicationForm/Skills.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -275,6 +276,24 @@ export default ({ refs, errors, formInputs, onChange, role, handleResume }) => {
customRef={refs['longAnswers2Ref']}
/>
</FormGroup>
<FormGroup>
<QuestionHeading>question 21</QuestionHeading>
<SubHeading size="1.25em">Draw your favourite character!</SubHeading>
<SketchCanvas
width={'100%'}
height={400}
value={formInputs.longAnswers3}
invalid={!!errors.longAnswers3}
errorMsg={errors.longAnswers3}
onChange={val =>
onChange({
longAnswers3: val,
})
}
customRef={refs['longAnswers3Ref']}
exportAsBase64
Comment thread
meleongg marked this conversation as resolved.
/>
</FormGroup>
</FormSpacing>
</>
)
Expand Down
235 changes: 235 additions & 0 deletions src/components/Input/SketchCanvas.js
Original file line number Diff line number Diff line change
@@ -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
Comment thread
meleongg marked this conversation as resolved.
* @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 (
<CanvasContainer
style={{
width: width,
height: height,
border: invalid ? '3px solid red' : '',
}}
onClick={() => {
setShowPicker(false)
}}
>
{errorMsg && <ErrorMessage style={{ width: width }}>{errorMsg}</ErrorMessage>}

<ReactSketchCanvas
width={'100%'}
height={'100%'}
ref={mergeRefs(canvasRef, customRef)}
strokeWidth={strokeWidth}
strokeColor={strokeColor}
onChange={handleChange}
style={{ border: 'none' }}
/>

<ToolBar>
<LeftIcons>
<CanvasIcon
src={colorpickerIcon}
style={{
marginRight: 10,
filter: showPicker
? 'invert(31%) sepia(79%) saturate(7412%) hue-rotate(241deg) brightness(84%) contrast(127%)'
: 'invert(72%) sepia(0%) saturate(237%) hue-rotate(137deg) brightness(86%) contrast(87%)',
Comment thread
lsha0730 marked this conversation as resolved.
}}
alt="Color Picker Icon"
onClick={e => {
e.stopPropagation()
setShowPicker(!showPicker)
}}
/>
<Stroke4Button
onClick={() => {
setStrokeWidth(4)
}}
/>
<Stroke6Button
onClick={() => {
setStrokeWidth(6)
}}
/>
<Stroke8Button
onClick={() => {
setStrokeWidth(8)
}}
/>
</LeftIcons>

<HistoryIcons>
<CanvasIcon src={undoIcon} alt="Undo Icon" onClick={handleUndo} />
<CanvasIcon src={redoIcon} alt="Redo Icon" onClick={handleRedo} />
</HistoryIcons>

<CanvasIcon
src={trashcanIcon}
style={{
filter:
'invert(61%) sepia(100%) saturate(6853%) hue-rotate(344deg) brightness(124%) contrast(108%)',
}}
alt="Trashcan Icon"
onClick={handleClear}
/>

{showPicker && (
<ColorPickerWrapper
onClick={e => {
e.stopPropagation()
}}
>
<SketchPicker
color={strokeColor}
onChange={e => {
setStrokeColor(e.hex)
}}
/>
</ColorPickerWrapper>
)}
</ToolBar>
</CanvasContainer>
)
}
8 changes: 8 additions & 0 deletions src/utility/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Loading