├── .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 |
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 | You need to enable JavaScript to run this app.
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 |
24 |
25 | Export canvas as image
26 |
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 | handleUpload(e)}
41 | >
42 |
48 |
49 | Upload
50 |
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 |
25 |
26 | All
27 | {categoryArray.map((val, i) => (
28 |
29 | {val}
30 |
31 | ))}
32 |
33 |
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 |
--------------------------------------------------------------------------------