├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── images │ ├── animecat.png │ ├── awge.png │ ├── background1.jpg │ ├── background2.jpg │ ├── background3.jpg │ ├── cat.png │ ├── doge.png │ ├── eyes.png │ ├── kakashi.png │ ├── kanye.png │ ├── levi.png │ ├── testing.png │ └── tyler.png ├── index.html ├── logo128.png ├── logo512.png ├── logo64.png ├── manifest.json └── robots.txt └── src ├── App.js ├── Assets ├── README.md └── Styles │ └── index.css ├── Components ├── Canvas.js ├── CanvasBackground.js ├── ImageComponent.js ├── ItemsList.js ├── ItemsListComponents │ ├── BackgroundsSection.js │ ├── ImagesSection.js │ ├── ShareSection.js │ ├── UploadSection.js │ └── filterBar.js ├── Styles │ ├── canvas.css │ └── itemsList.css └── ToolsBar.js ├── Data ├── items.json └── tools.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # misc 9 | .DS_Store 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | Logo 6 | 7 | 8 |

React Konva Moodboard

9 | 10 |

11 | This is pure front-end project builded in React 12 |
13 | It uses React-Konva as main functionality 14 |
15 | It is used for making custom moodboards with images 16 |
17 | It can be used in business like home decor for helping understanding client's vision 18 |

19 |
20 | 21 | 22 | 23 | 24 | ## Showcase of project 25 | 26 |

Adding local images to canvas

27 |

Images can be uploaded to Konva stage via drag and drop or by clicking them.

28 | 29 |
30 | 31 |

Changing background of canvas

32 |

React-Konva way of adding background is by creating "Rect" component at the very bottom of the elements and filling it with image

33 | 34 | 35 |

Uploading custom images

36 |

Application support custom uploaded images from local drive

37 |

Uploaded images are stored in localStorage of the browser

38 |

Because react renders were clearing images when tab was switched

39 | 40 | 41 |

Exporting canvas to image

42 |

Canvas image can be easilly exported to an image

43 | 44 | 45 |

Responsive showcase

46 |

The biggest problem I had to deal with, was making Konva stage responsive

47 |

The problem was to keep canvas size synchronized with it's bitmap aswell with scale of images

48 |

When bitmap wasn't calculated, moving or resizng images wasn't possible

49 | 50 | 51 |

(back to top)

52 | 53 | 54 | 55 | ### Built With 56 | 57 | * [React.js](https://reactjs.org/) 58 | * [React-Konva](https://konvajs.org/docs/react/index.html) 59 | 60 | 61 |

(back to top)

62 | 63 | 64 | 65 | 66 | ### Installation 67 | 68 | 1. Clone the repo 69 | ```sh 70 | git clone https://github.com/Zlvsky/React-Konva-moodboard.git 71 | ``` 72 | 2. Install NPM packages 73 | ```sh 74 | npm install 75 | ``` 76 | 3. Run app 77 | ```sh 78 | npm start 79 | ``` 80 |

To add local images, upload them to /public/images and declare them in /src/Data/items.json

81 | 82 |

(back to top)

83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-moodboard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.8.1", 7 | "@emotion/styled": "^11.8.1", 8 | "@mui/icons-material": "^5.4.4", 9 | "@mui/material": "^5.4.4", 10 | "@testing-library/jest-dom": "^5.16.2", 11 | "@testing-library/react": "^12.1.3", 12 | "@testing-library/user-event": "^13.5.0", 13 | "konva": "^8.3.3", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-konva": "^17.0.2-5", 17 | "react-scripts": "5.0.0", 18 | "use-image": "^1.0.10", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/favicon.ico -------------------------------------------------------------------------------- /public/images/animecat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/animecat.png -------------------------------------------------------------------------------- /public/images/awge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/awge.png -------------------------------------------------------------------------------- /public/images/background1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/background1.jpg -------------------------------------------------------------------------------- /public/images/background2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/background2.jpg -------------------------------------------------------------------------------- /public/images/background3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/background3.jpg -------------------------------------------------------------------------------- /public/images/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/cat.png -------------------------------------------------------------------------------- /public/images/doge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/doge.png -------------------------------------------------------------------------------- /public/images/eyes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/eyes.png -------------------------------------------------------------------------------- /public/images/kakashi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/kakashi.png -------------------------------------------------------------------------------- /public/images/kanye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/kanye.png -------------------------------------------------------------------------------- /public/images/levi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/levi.png -------------------------------------------------------------------------------- /public/images/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/testing.png -------------------------------------------------------------------------------- /public/images/tyler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/images/tyler.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | DragNDrop Vision 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/logo128.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/logo512.png -------------------------------------------------------------------------------- /public/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zlvsky/React-Konva-moodboard/73d1f494212e3be5a1c0277a57adf5f22d80da0c/public/logo64.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "DragNDrop Vision", 3 | "name": "Drag and drop your vision", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo128.png", 12 | "type": "image/png", 13 | "sizes": "128x128" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Canvas from './Components/Canvas'; 3 | 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default App 14 | -------------------------------------------------------------------------------- /src/Assets/README.md: -------------------------------------------------------------------------------- 1 | Local images are stored in /public/images -------------------------------------------------------------------------------- /src/Assets/Styles/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --matDark: #252627; 3 | --dark: #18191B; 4 | --gray: #959596; 5 | } 6 | body { 7 | margin: 0; 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 9 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 10 | sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 17 | monospace; 18 | } 19 | 20 | 21 | .bodyWrap { 22 | background-color: #EBECF0; 23 | width: 100vw; 24 | height: 100vh; 25 | } 26 | -------------------------------------------------------------------------------- /src/Components/Canvas.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from "react"; 2 | import { Stage, Layer } from "react-konva"; 3 | import "./Styles/canvas.css"; 4 | import ItemsList from "./ItemsList"; 5 | import ImageComponent from "./ImageComponent"; 6 | import CanvasBackground from "./CanvasBackground"; 7 | 8 | function Canvas() { 9 | // static canvas dimensions used for scaling ratio 10 | const stageWidth = 900, 11 | stageHeight = 600; 12 | // dynamic canvas dimensions 13 | const [stageDimensions, setStageDimensions] = useState({ 14 | width: stageWidth, 15 | height: stageHeight, 16 | scale: 1 17 | }); 18 | // stageRef is used for handling callbacks - example: getting canvas positions after drag and rop 19 | const stageRef = useRef(); 20 | // containerRef is used for dynamic canvas scalling 21 | // main purpose of containerRef is to get width of parent div of canvas stage 22 | const containerRef = useRef(); 23 | // dragUrl stores temporary src of dragged image 24 | const [dragUrl, setDragUrl] = useState(); 25 | // images stores images that are added to canvas 26 | const [images, setImages] = useState([]); 27 | // backgroundImage is used for setting backgroundImage of canvas 28 | const [backgroundImage, setBackgroundImage] = useState(); 29 | // selectedId is used for keeping selected image to handle resizes, z-index priority etc. 30 | const [selectedId, setSelectedId] = useState(null); 31 | 32 | // function to handle resize of canvas dimensions based on window width or when sidebar is closed or opened 33 | const handleResize = () => { 34 | let sceneWidth = containerRef.current.clientWidth; 35 | let scale = sceneWidth / stageWidth; 36 | setStageDimensions({ 37 | width: stageWidth * scale, 38 | height: stageHeight * scale, 39 | scale: scale, 40 | }); 41 | }; 42 | 43 | // add eventListener for every window resize to call handleResize function 44 | useEffect(() => { 45 | handleResize(); 46 | window.addEventListener("resize", handleResize, false); 47 | return () => window.addEventListener("resize", handleResize, false); 48 | }, []); 49 | 50 | // if clicked on empty space of canvas, including backgroundImage perform deselect item 51 | const checkDeselect = (e) => { 52 | const clickedOnEmpty = e.target === e.target.getStage(); 53 | const clikedOnBackground = e.target.getId() === "canvasBackground"; 54 | if (clickedOnEmpty || clikedOnBackground) { 55 | setSelectedId(null); 56 | } 57 | }; 58 | 59 | // when element is dragged pass its image src to allow it for adding it to canvas 60 | const onChangeDragUrl = (dragUrl) => { 61 | setDragUrl(dragUrl); 62 | }; 63 | 64 | // update image attributes when performing resize 65 | const handleTransformChange = (newAttrs, i) => { 66 | let imagesToUpdate = images; 67 | let singleImageToUpdate = imagesToUpdate[i]; 68 | // update old attributes 69 | singleImageToUpdate = newAttrs; 70 | imagesToUpdate[i] = singleImageToUpdate; 71 | setImages(imagesToUpdate); 72 | }; 73 | 74 | // function to handle adding images on drag and drop to canvas 75 | const handleOnDrop = (e) => { 76 | e.preventDefault(); 77 | stageRef.current.setPointersPositions(e); 78 | setImages( 79 | images.concat([ 80 | { 81 | ...stageRef.current.getPointerPosition(), 82 | src: dragUrl, 83 | }, 84 | ]) 85 | ); 86 | }; 87 | 88 | // function to handle adding images on click 89 | const handleAddOnClick = (src) => { 90 | let centerX = stageDimensions.width / 2 91 | let centerY = stageDimensions.height / 2 92 | setImages( 93 | images.concat([ 94 | { 95 | x: centerX, 96 | y: centerY, 97 | src: src, 98 | }, 99 | ]) 100 | ); 101 | } 102 | 103 | // function to handle adding background image of canvas 104 | const addToBackground = (backgroundUrl) => { 105 | setBackgroundImage(backgroundUrl); 106 | }; 107 | 108 | // function to handle removing background image of canvas 109 | const removeBackground = () => { 110 | setBackgroundImage(null) 111 | }; 112 | 113 | // used for passing image id to image attributes 114 | const passImageWithId = (image, id) => { 115 | const imageWithId = { 116 | ...image, 117 | id: id, 118 | }; 119 | return imageWithId; 120 | }; 121 | 122 | // when sidebar state changes this function is being called 123 | const resizeCanvasOnSidebarChange = () => { 124 | // wait for sidebar animation to complete 125 | setTimeout(() => { 126 | handleResize(); 127 | }, 420); 128 | } 129 | 130 | return ( 131 |
132 | 141 | 142 |
143 |
e.preventDefault()} 148 | > 149 | { 157 | // deselect when clicked on empty area or background image 158 | checkDeselect(e); 159 | }} 160 | > 161 | 162 | {typeof backgroundImage === "string" && ( 163 | // check if background image is not empty, default state is null 164 | 169 | )} 170 | {images.map((image, i) => { 171 | return ( 172 | { 179 | setSelectedId(i); 180 | }} 181 | onChange={(newAttrs) => { 182 | handleTransformChange(newAttrs, i); 183 | }} 184 | /> 185 | ); 186 | })} 187 | 188 | 189 |
190 |
191 |
192 | ); 193 | } 194 | 195 | export default Canvas; 196 | -------------------------------------------------------------------------------- /src/Components/CanvasBackground.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Rect } from 'react-konva'; 3 | import useImage from 'use-image'; 4 | 5 | // Konva way of adding background image to canvas 6 | // creating Rect Konva component 7 | // placing it at the bottom of all elements 8 | // changing its z-index to lowest value 9 | 10 | function CanvasBackground({ backgroundUrl, width, height }) { 11 | // create image of image src 12 | const [background] = useImage(backgroundUrl); 13 | // calculations to fill the whole area of canvas 14 | let widthRatio = 1, heightRatio = 1; 15 | if(background !== undefined) { 16 | widthRatio = width / background.width; 17 | heightRatio = height / background.height; 18 | } 19 | return ( 20 | 32 | 33 | ) 34 | } 35 | 36 | export default CanvasBackground 37 | -------------------------------------------------------------------------------- /src/Components/ImageComponent.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, Fragment } from 'react'; 2 | import { Image, Transformer } from "react-konva"; 3 | import useImage from 'use-image'; 4 | 5 | // image component that contains various event handlers 6 | // image component is used for passing it to Konva canvas 7 | 8 | const ImageComponent = ({ image, shapeProps, id, isSelected, onSelect, onChange }) => { 9 | // creating image based on its src 10 | const [img] = useImage(image.src); 11 | const shapeRef = useRef(); 12 | const transformRef = useRef(); 13 | 14 | // if selected create box around the image to allow performing resizes 15 | useEffect(() => { 16 | if (isSelected) { 17 | transformRef.current.setNode(shapeRef.current); 18 | transformRef.current.getLayer().batchDraw() 19 | } 20 | }, [isSelected]); 21 | 22 | // if dropped on konva stage pass its attributes like src, width, height, x and y 23 | const handleOnDrop = e => { 24 | onChange({ 25 | ...shapeProps, 26 | x: e.target.x(), 27 | y: e.target.y() 28 | }); 29 | } 30 | 31 | // called when dragging starts image in konva Canvas 32 | const handleDragStart = e => { 33 | // move dragged images on top 34 | onChange({ 35 | ...shapeProps, 36 | x: e.target.x(), 37 | y: e.target.y() 38 | }) 39 | onSelect(e); 40 | e.target.moveToTop(); 41 | 42 | 43 | // creates shadow around the image 44 | e.target.setAttrs({ 45 | shadowOffset: { 46 | x: 0, 47 | y: 0 48 | }, 49 | scaleX: 1.05, 50 | scaleY: 1.05, 51 | shadowBlur: 16, 52 | ShadowOpacity: 0.6 53 | }); 54 | }; 55 | 56 | // called when dragging ends 57 | const handleDragEnd = e => { 58 | // clear shadow around the image 59 | e.target.to({ 60 | duration: 0.1, 61 | scaleX: 1, 62 | scaleY: 1, 63 | shadowOffsetX: 0, 64 | shadowOffsetY: 4, 65 | shadowBlur: 10, 66 | ShadowOpacity: 0.4 67 | }); 68 | 69 | // updates the position 70 | onChange({ 71 | ...shapeProps, 72 | x: e.target.x(), 73 | y: e.target.y() 74 | }); 75 | }; 76 | 77 | 78 | // called when performed resize 79 | const handleTransformOnEnd = e => { 80 | // node - refference to image 81 | const node = shapeRef.current; 82 | const scaleX = node.scaleX(); 83 | const scaleY = node.scaleY(); 84 | node.scaleX(1); 85 | node.scaleY(1); 86 | node.width(Math.max(5, node.width() * scaleX)); 87 | node.height(Math.max(node.height() * scaleY)); 88 | onChange({ 89 | ...shapeProps, 90 | x: node.x(), 91 | y: node.y(), 92 | // set minimal value 93 | width: node.width(), 94 | height: node.height() 95 | }); 96 | } 97 | 98 | return ( 99 | 100 | 118 | {isSelected && ( 119 | // when selected it creates box around the image to perform resizes 120 | { 123 | // limit resize 124 | if (newBox.width < 5 || newBox.height < 5) { 125 | return oldBox; 126 | } 127 | return newBox; 128 | }} 129 | /> 130 | )} 131 | 132 | ); 133 | }; 134 | 135 | export default ImageComponent; 136 | -------------------------------------------------------------------------------- /src/Components/ItemsList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import './Styles/itemsList.css'; 3 | import { tools } from '../Data/tools'; 4 | 5 | import ImagesSection from './ItemsListComponents/ImagesSection'; 6 | import BackgroundsSection from './ItemsListComponents/BackgroundsSection'; 7 | import UploadSection from "./ItemsListComponents/UploadSection"; 8 | import ShareSection from "./ItemsListComponents/ShareSection"; 9 | import ToolsBar from './ToolsBar'; 10 | 11 | import ExpandLessRoundedIcon from "@mui/icons-material/ExpandLessRounded"; 12 | 13 | function ItemsList(props) { 14 | const [selectedTools, setSelectedTools] = useState(0); 15 | // componentsMap keys must be same with components key value in /Data/tools.js 16 | const componentsMap = { 17 | imagesSection: ImagesSection, 18 | backgroundsSection: BackgroundsSection, 19 | uploadSection: UploadSection, 20 | shareSection: ShareSection 21 | }; 22 | const [sidebarCollapse, setSidebarCollapse] = useState(true); 23 | 24 | const changeSelectedTool = (id) => { 25 | setSelectedTools(id) 26 | } 27 | 28 | const openMenuOnClick = () => { 29 | sidebarCollapse ? setSidebarCollapse(false) : setSidebarCollapse(true); 30 | } 31 | 32 | const handleCanvasResizeOnSidebarChange = () => { 33 | props.resizeCanvasOnSidebarChange(); 34 | } 35 | 36 | // everytime when sidebar state changes function in Canvas.js is being called for resizing canvas dimensions 37 | useEffect(() => { 38 | handleCanvasResizeOnSidebarChange(); 39 | }, [sidebarCollapse]); 40 | 41 | 42 | return ( 43 |
48 |
openMenuOnClick()}>
49 |
50 | 51 | 52 | {tools.map((val) => { 53 | if (val.id === selectedTools) { 54 | const Component = componentsMap[val.component]; 55 | return ( 56 | 65 | ); 66 | } 67 | })} 68 |
69 |
70 | ); 71 | } 72 | 73 | export default ItemsList; 74 | -------------------------------------------------------------------------------- /src/Components/ItemsListComponents/BackgroundsSection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import items from "../../Data/items.json"; 3 | 4 | function BackgroundsSection(props) { 5 | const backgroundsFilteredArray = items.filter( 6 | (el) => el.elementCategory === "backgrounds" 7 | ); 8 | 9 | return ( 10 |
11 |
12 | { 15 | props.removeBackground(); 16 | }} 17 | > 18 | Click to clear background 19 | 20 |
21 |
22 | {backgroundsFilteredArray.map((item, i) => ( 23 |
24 | { 31 | props.addToBackground(e.target.src); 32 | }} 33 | /> 34 |
35 | ))} 36 |
37 |
38 | ); 39 | } 40 | 41 | export default BackgroundsSection; 42 | -------------------------------------------------------------------------------- /src/Components/ItemsListComponents/ImagesSection.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import items from "../../Data/items.json"; 3 | import FilterBar from "./filterBar"; 4 | 5 | // images can be dragged or cliked for adding it to canvas 6 | 7 | function ImagesSection(props) { 8 | const [selectedCategory, setSelectedCategory] = useState("") 9 | 10 | // get images that are declared as photos from /Data/items.json 11 | const photosFilteredArray = items.filter( 12 | (el) => el.elementCategory === "photos" 13 | ); 14 | 15 | // filter images by selected category 16 | const filterImagesByCategories = (array) => { 17 | let filteredArrayToReturn; 18 | if(selectedCategory.length > 0) { 19 | filteredArrayToReturn = array.filter( 20 | (el) => el.photoCategory === selectedCategory 21 | ) 22 | return filteredArrayToReturn; 23 | } 24 | return array; 25 | } 26 | // array of images ready to display 27 | const arrayToDisplay = filterImagesByCategories(photosFilteredArray) 28 | 29 | 30 | 31 | return ( 32 |
33 | { 36 | setSelectedCategory(selectedCategory) 37 | }} 38 | /> 39 | 40 |
41 | {arrayToDisplay.map((item, i) => ( 42 |
43 | { 50 | props.onChangeDragUrl(e.target.src); 51 | }} 52 | onClick={(e) => { 53 | props.handleAddOnClick(e.target.src); 54 | }} 55 | /> 56 |
57 | ))} 58 |
59 |
60 | ); 61 | } 62 | 63 | export default ImagesSection; 64 | -------------------------------------------------------------------------------- /src/Components/ItemsListComponents/ShareSection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DownloadRoundedIcon from "@mui/icons-material/DownloadRounded"; 3 | 4 | function ShareSection(props) { 5 | // function that creates hyperlink with canvas DataURL as href 6 | // programically clicks it to download the image 7 | // after download hyperlink is being removed from DOM 8 | const handleExport = () => { 9 | const uri = props.stageRef.current.toDataURL(); 10 | const link = document.createElement("a"); 11 | console.log(uri); 12 | console.log(link); 13 | link.download = "moodboard-export.png"; 14 | link.href = uri; 15 | document.body.appendChild(link); 16 | link.click(); 17 | document.body.removeChild(link); 18 | }; 19 | 20 | return ( 21 |
22 |
23 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default ShareSection; -------------------------------------------------------------------------------- /src/Components/ItemsListComponents/UploadSection.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import UploadRoundedIcon from "@mui/icons-material/UploadRounded"; 3 | 4 | // images can be uploaded to client side manually 5 | // for temporary storing data of images I used localstorage 6 | 7 | function UploadSection(props) { 8 | const [uploadedImages, setUploadedImage] = useState([]); 9 | 10 | // clear localstorage on tab close to prevent loading blank images 11 | window.onbeforeunload = function () { 12 | const emptyArray = [] 13 | localStorage.setItem("uploadedImages", JSON.stringify(emptyArray)); 14 | }; 15 | 16 | // saving state to local storage to prevent deleting uploaded images after closing tab with "uploads" 17 | useEffect(() => { 18 | const loadImages = JSON.parse(localStorage.getItem("uploadedImages")); 19 | setUploadedImage(loadImages); 20 | }, []); 21 | 22 | // adding images to localstorage every upload 23 | useEffect(() => { 24 | localStorage.setItem( 25 | "uploadedImages", 26 | JSON.stringify(uploadedImages) 27 | ); 28 | }, [uploadedImages]); 29 | 30 | const UploadButton = () => { 31 | const handleUpload = (e) => { 32 | let img = e.target.files[0]; 33 | setUploadedImage((prevState) => [...prevState, URL.createObjectURL(img)]); 34 | }; 35 | return ( 36 |
37 | 51 |
52 | ); 53 | }; 54 | 55 | const UploadedImages = () => { 56 | return uploadedImages?.map((item, i) => ( 57 |
58 | { 65 | props.onChangeDragUrl(e.target.src); 66 | }} 67 | onClick={(e) => { 68 | props.handleAddOnClick(e.target.src); 69 | }} 70 | /> 71 |
72 | )); 73 | }; 74 | 75 | // check if localstorage is empty, if empty display tooltip instead array of images 76 | const checkUploadedImagesNotEmpty = () => { 77 | if (uploadedImages.length > 0) { 78 | return true; 79 | } 80 | return false; 81 | }; 82 | 83 | 84 | return ( 85 |
86 | 87 | {checkUploadedImagesNotEmpty() ? ( 88 | 89 | ) : ( 90 |

Upload your images with button above.

91 | )} 92 |
93 | ); 94 | } 95 | 96 | export default UploadSection; 97 | -------------------------------------------------------------------------------- /src/Components/ItemsListComponents/filterBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // simple filterBar for conditional rendering images by categories in sidebar 4 | 5 | function FilterBar(props) { 6 | // function to create array of list of uniques categories based on images categories in /Data/items.json 7 | const itemsWithoutDuplicates = (itemsToReturn) => { 8 | let categoryArray = []; 9 | itemsToReturn.forEach((element) => { 10 | let photoCategroy = element.photoCategory; 11 | let isInArray = categoryArray.indexOf(photoCategroy); 12 | if (isInArray === -1) { 13 | categoryArray.push(photoCategroy); 14 | } 15 | }); 16 | return categoryArray; 17 | }; 18 | const categoryArray = itemsWithoutDuplicates(props.items); 19 | const handleFilterChange = (e) => { 20 | props.onChange(e.target.value); 21 | }; 22 | 23 | return ( 24 | 34 | ); 35 | } 36 | 37 | export default FilterBar; 38 | -------------------------------------------------------------------------------- /src/Components/Styles/canvas.css: -------------------------------------------------------------------------------- 1 | .workContainer { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | flex-direction: row; 7 | } 8 | .canvasWrap { 9 | display: flex; 10 | flex-grow: 1; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | .canvasBody { 15 | position: relative; 16 | width: 80%; 17 | max-width: 900px; 18 | } 19 | 20 | .canvasStage canvas { 21 | background-color: white !important; 22 | box-shadow: 0 2px 8px rgb(14 19 24 / 7%); 23 | } 24 | 25 | /* responsive */ 26 | @media screen and (max-width: 1024px) { 27 | .workContainer { 28 | flex-direction: column-reverse; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Components/Styles/itemsList.css: -------------------------------------------------------------------------------- 1 | /* general css */ 2 | .itemsListWrap { 3 | height: 100%; 4 | z-index: 1; 5 | display: flex; 6 | transition: all .4s ease-in-out; 7 | overflow: hidden; 8 | flex-direction: row-reverse; 9 | } 10 | .itemsListBody { 11 | background: var(--dark); 12 | overflow: hidden; 13 | height: 100%; 14 | width: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | 18 | } 19 | /* end of general css */ 20 | 21 | /* toolsBar css */ 22 | .toolsBarWrap { 23 | display: flex; 24 | flex-direction: row; 25 | height: 72px; 26 | position: relative; 27 | } 28 | .toolsBarBody { 29 | min-width: 72px; 30 | display: flex; 31 | flex-direction: row; 32 | flex-grow: 0; 33 | position: relative; 34 | height: 100%; 35 | } 36 | .toolsItemsWrap { 37 | display: flex; 38 | flex-direction: row; 39 | } 40 | .toolsItem { 41 | height: 72px; 42 | width: 72px; 43 | position: relative; 44 | } 45 | .toolsItemContent { 46 | font-size: 1.1rem; 47 | display: flex; 48 | flex-direction: column; 49 | flex-wrap: wrap; 50 | justify-content: center; 51 | align-items: center; 52 | color: var(--gray); 53 | cursor: pointer; 54 | margin-top: 20px; 55 | } 56 | .toolIcon { 57 | width: 24px; 58 | height: 24px; 59 | } 60 | .toolTitle { 61 | display: block; 62 | padding: 0 2px; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | max-width: 100%; 66 | height: 18px; 67 | line-height: 18px; 68 | text-overflow: ellipsis; 69 | text-align: center; 70 | font-size: 11px; 71 | } 72 | /* end of toolBar css */ 73 | 74 | /* itemsSection css */ 75 | /* select filter */ 76 | .categorySelectLabel { 77 | position: relative; 78 | display: block; 79 | margin: 50px auto 50px; 80 | width: 90%; 81 | } 82 | .categorySelectLabel::after { 83 | content: '▼'; 84 | position: absolute; 85 | width: 27px; 86 | color: #999; 87 | font-weight: bold; 88 | font-size: 16px; 89 | right: 0px; 90 | bottom: 8px; 91 | -webkit-border-radius: 3px; 92 | -moz-border-radius: 3px; 93 | border-radius: 3px; 94 | pointer-events: none; 95 | z-index: 2; 96 | } 97 | .categorySelectLabel::before { 98 | content: ''; 99 | right: 2px; 100 | top: 2px; 101 | width: 38px; 102 | height: 34px; 103 | background: #242424; 104 | position: absolute; 105 | pointer-events: none; 106 | display: block; 107 | z-index: 1; 108 | -webkit-border-radius: 3px; 109 | -moz-border-radius: 3px; 110 | border-radius: 3px; 111 | } 112 | .categorySelect { 113 | position: relative; 114 | width: 100%; 115 | -webkit-appearance: none; 116 | -moz-appearance: none; 117 | appearance: none; 118 | background: #111; 119 | color: #999; 120 | border: none; 121 | outline: none; 122 | font-size: 14px; 123 | padding: 10px 9px; 124 | margin: 0; 125 | -webkit-border-radius: 3px; 126 | -moz-border-radius: 3px; 127 | border-radius: 3px; 128 | cursor: pointer; 129 | height: 38px; 130 | } 131 | .categorySelect option { 132 | font-size: 17px; 133 | } 134 | /* end of select */ 135 | /* backgrounds text */ 136 | .clearBackgroundWrap { 137 | position: relative; 138 | display: block; 139 | margin: 50px auto 50px; 140 | width: 90%; 141 | text-align: center; 142 | } 143 | .clearBackgroundText { 144 | color: #fff; 145 | font-size: 18px; 146 | cursor: pointer; 147 | transition: color .15s ease-in-out; 148 | } 149 | .clearBackgroundText:hover { 150 | color:#999 151 | } 152 | .clearBackgroundText::before { 153 | content: '\261B'; 154 | } 155 | /* end of backgrounds text */ 156 | /* upload image */ 157 | .uploadImageWrap { 158 | position: relative; 159 | display: block; 160 | margin: 50px auto 50px; 161 | width: 90%; 162 | text-align: center; 163 | } 164 | .uploadImageButton { 165 | padding: 10px 50px; 166 | border-radius: 15px; 167 | color: white; 168 | background-color: #EF5F63; 169 | font-weight: 600; 170 | display: flex; 171 | flex-direction: row; 172 | justify-content: center; 173 | align-items: center; 174 | transition: background-color .15s ease-in-out; 175 | cursor: pointer; 176 | } 177 | .uploadImageButton:hover { 178 | background-color: #c04c50; 179 | } 180 | .uploadTooltip { 181 | color: #fff; 182 | text-align: center; 183 | } 184 | /* end of upload image */ 185 | .itemsSection { 186 | height: 100%; 187 | background-color: var(--matDark); 188 | padding: 10px 5px; 189 | display: inline-block; 190 | } 191 | .itemsWrapper { 192 | overflow: auto; 193 | max-height: calc(95% - 150px); 194 | } 195 | .itemsImage { 196 | height: 100px; 197 | max-width: 200px; 198 | object-fit: contain; 199 | border: 1px solid var(--gray); 200 | border-radius: 10px; 201 | margin: 5px 0; 202 | } 203 | .imageContainer { 204 | display: inline-block; 205 | width: max-content; 206 | margin: 0 5px; 207 | } 208 | /* share section */ 209 | .shareSectionWrap { 210 | position: relative; 211 | display: block; 212 | margin: 50px auto 50px; 213 | width: 90%; 214 | text-align: center; 215 | } 216 | .downloadImage { 217 | background-color: #EF5F63; 218 | border: none; 219 | color: white; 220 | padding: 15px 32px; 221 | margin: 0 auto; 222 | text-align: center; 223 | text-decoration: none; 224 | display: inline-block; 225 | font-size: 16px; 226 | border-radius: 15px; 227 | font-weight: 600; 228 | display: flex; 229 | flex-direction: row; 230 | justify-content: center; 231 | align-items: center; 232 | transition: background-color .15s ease-in-out; 233 | cursor: pointer; 234 | } 235 | .downloadImage:hover { 236 | background-color: #c04c50; 237 | } 238 | /* end of sharing section */ 239 | /* expand button */ 240 | .expandButton { 241 | width: auto; 242 | height: 100px; 243 | margin: auto 0; 244 | background-color: #252627; 245 | display: flex; 246 | justify-content: center; 247 | align-items: center; 248 | color: white; 249 | border-radius: 0 50px 50px 0; 250 | cursor: pointer; 251 | } 252 | .sidebarOpen .expandButton svg { 253 | transform: rotate(270deg); 254 | } 255 | .sidebarClosed .expandButton svg { 256 | transform: rotate(90deg); 257 | } 258 | .expandButton svg { 259 | transition: all .5s ease-in-out; 260 | } 261 | /* end of expand button */ 262 | 263 | /* end of itemsSection css */ 264 | 265 | /* responsive */ 266 | .sidebarOpen { 267 | width: 40%; 268 | max-width: 432px; 269 | } 270 | .sidebarClosed { 271 | width: 24px; 272 | max-width: 432px; 273 | } 274 | @media screen and (max-width: 1024px) { 275 | .itemsListWrap { 276 | height: 90%; 277 | position: absolute; 278 | flex-direction: column; 279 | } 280 | .sidebarOpen { 281 | width: 100%; 282 | max-width: none; 283 | } 284 | .sidebarClosed { 285 | height: 15%; 286 | width: 100%; 287 | max-width: none; 288 | } 289 | .sidebarOpen .expandButton svg { 290 | transform: rotate(0); 291 | } 292 | .sidebarClosed .expandButton svg { 293 | transform: rotate(180deg); 294 | } 295 | .expandButton { 296 | border-radius: 50px 50px 0 0; 297 | width: 100px; 298 | height: auto; 299 | margin: 0 auto; 300 | } 301 | } 302 | /* end of responsive */ -------------------------------------------------------------------------------- /src/Components/ToolsBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { tools } from '../Data/tools'; 3 | 4 | // list of tools imported from /Data/tools.js 5 | 6 | function ToolsBar(props) { 7 | return ( 8 |
9 |
10 |
11 | {tools.map((tool, i) => ( 12 |
{ 16 | props.changeSelectedTool(i) 17 | }} 18 | > 19 |
20 | 21 | {tool.icon} 22 | 23 | 24 | {tool.title} 25 | 26 |
27 |
28 | ))} 29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default ToolsBar; 36 | -------------------------------------------------------------------------------- /src/Data/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Kakashi", 4 | "elementCategory": "photos", 5 | "photoCategory": "anime", 6 | "source": "images/kakashi.png" 7 | }, 8 | { 9 | "name": "Anime cat", 10 | "elementCategory": "photos", 11 | "photoCategory": "anime", 12 | "source": "images/animecat.png" 13 | }, 14 | { 15 | "name": "Cat", 16 | "elementCategory": "photos", 17 | "photoCategory": "animals", 18 | "source": "images/cat.png" 19 | }, 20 | { 21 | "name": "Tyler", 22 | "elementCategory": "photos", 23 | "photoCategory": "people", 24 | "source": "images/tyler.png" 25 | }, 26 | { 27 | "name": "Testing ASAP Rocky", 28 | "elementCategory": "photos", 29 | "photoCategory": "other", 30 | "source": "images/testing.png" 31 | }, 32 | { 33 | "name": "Levi Ackerman", 34 | "elementCategory": "photos", 35 | "photoCategory": "anime", 36 | "source": "images/levi.png" 37 | }, 38 | { 39 | "name": "Kanye West", 40 | "elementCategory": "photos", 41 | "photoCategory": "people", 42 | "source": "images/kanye.png" 43 | }, 44 | { 45 | "name": "Eyes", 46 | "elementCategory": "photos", 47 | "photoCategory": "other", 48 | "source": "images/eyes.png" 49 | }, 50 | { 51 | "name": "Doge", 52 | "elementCategory": "photos", 53 | "photoCategory": "animals", 54 | "source": "images/doge.png" 55 | }, 56 | { 57 | "name": "Background", 58 | "elementCategory": "backgrounds", 59 | "source": "images/background1.jpg" 60 | }, 61 | { 62 | "name": "Background2", 63 | "elementCategory": "backgrounds", 64 | "source": "images/background2.jpg" 65 | }, 66 | { 67 | "name": "Background3", 68 | "elementCategory": "backgrounds", 69 | "source": "images/background3.jpg" 70 | }, 71 | { 72 | "name": "AWGE", 73 | "elementCategory": "photos", 74 | "photoCategory": "other", 75 | "source": "images/awge.png" 76 | } 77 | ] 78 | -------------------------------------------------------------------------------- /src/Data/tools.js: -------------------------------------------------------------------------------- 1 | import PhotoIcon from '@mui/icons-material/Photo'; 2 | import UploadFileRoundedIcon from "@mui/icons-material/UploadFileRounded"; 3 | import WallpaperIcon from '@mui/icons-material/Wallpaper'; 4 | import IosShareRoundedIcon from "@mui/icons-material/IosShareRounded"; 5 | // below is list of components that appear in sidebar 6 | // id - unique id 7 | // title - title of tool 8 | // icon - imported icon from material ui 9 | // component - component string needed for conditional rendering in itemsList.js 10 | export const tools = [ 11 | { 12 | id: 0, 13 | title: "Photos", 14 | icon: , 15 | component: "imagesSection", 16 | }, 17 | { 18 | id: 1, 19 | title: "Backgrounds", 20 | icon: , 21 | component: "backgroundsSection", 22 | }, 23 | { 24 | id: 2, 25 | title: "Uploads", 26 | icon: , 27 | component: "uploadSection", 28 | }, 29 | { 30 | id: 3, 31 | title: "Share", 32 | icon: , 33 | component: "shareSection", 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './Assets/Styles/index.css'; 4 | import App from './App'; 5 | 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | --------------------------------------------------------------------------------