├── .env.sample ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── craco.config.js ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── Container.tsx ├── Providers.tsx ├── Routes.tsx ├── assets │ └── images │ │ └── full-color.png ├── common │ └── interfaces.ts ├── components │ ├── Dropzone │ │ ├── DropZone.tsx │ │ └── index.ts │ ├── Loading │ │ ├── Loading.tsx │ │ └── index.ts │ └── icons │ │ ├── Background.tsx │ │ ├── Elements.tsx │ │ ├── Illustrations.tsx │ │ ├── Images.tsx │ │ ├── Logo.tsx │ │ ├── Pixabay.tsx │ │ ├── Search.tsx │ │ ├── Templates.tsx │ │ ├── Text.tsx │ │ ├── Uploads.tsx │ │ └── index.ts ├── constants │ ├── app-options.ts │ ├── contants.ts │ ├── editor.ts │ ├── fonts.ts │ └── format-sizes.ts ├── contexts │ └── AppContext.tsx ├── hooks │ └── useAppContext.tsx ├── index.tsx ├── interfaces │ └── editor.ts ├── logo.svg ├── react-app-env.d.ts ├── reportWebVitals.ts ├── scenes │ ├── Dashboard │ │ ├── Creations.tsx │ │ ├── Dashboard.tsx │ │ ├── Templates.tsx │ │ ├── Uploads.tsx │ │ └── index.ts │ └── Editor │ │ ├── Editor.tsx │ │ ├── components │ │ ├── Footer │ │ │ ├── Footer.tsx │ │ │ └── index.ts │ │ ├── Icons │ │ │ ├── Background.tsx │ │ │ ├── Backward.tsx │ │ │ ├── Bold.tsx │ │ │ ├── ChevronLeft.tsx │ │ │ ├── CopyStyle.tsx │ │ │ ├── Delete.tsx │ │ │ ├── Duplicate.tsx │ │ │ ├── Elements.tsx │ │ │ ├── FillColor.tsx │ │ │ ├── Forward.tsx │ │ │ ├── Images.tsx │ │ │ ├── Italic.tsx │ │ │ ├── Locked.tsx │ │ │ ├── Logo.tsx │ │ │ ├── Opacity..tsx │ │ │ ├── Redo.tsx │ │ │ ├── Spacing.tsx │ │ │ ├── Templates.tsx │ │ │ ├── Text.tsx │ │ │ ├── TextAlignCenter.tsx │ │ │ ├── TextAlignJustify.tsx │ │ │ ├── TextAlignLeft.tsx │ │ │ ├── TextAlignRight.tsx │ │ │ ├── TextColor.tsx │ │ │ ├── TextSpacing.tsx │ │ │ ├── TimeFast.tsx │ │ │ ├── ToBack.tsx │ │ │ ├── ToFront.tsx │ │ │ ├── Underline.tsx │ │ │ ├── Undo.tsx │ │ │ ├── Unlocked.tsx │ │ │ ├── Uploads.tsx │ │ │ └── index.ts │ │ ├── Navbar │ │ │ ├── Navbar.tsx │ │ │ ├── components │ │ │ │ ├── File.tsx │ │ │ │ ├── History.tsx │ │ │ │ ├── PreviewTemplate.tsx │ │ │ │ └── Resize.tsx │ │ │ └── index.ts │ │ ├── Panels │ │ │ ├── PanelItem.tsx │ │ │ ├── PanelItems │ │ │ │ ├── Animations.tsx │ │ │ │ ├── Background.tsx │ │ │ │ ├── Color.tsx │ │ │ │ ├── Elements.tsx │ │ │ │ ├── FontFamily.tsx │ │ │ │ ├── Illustrations.tsx │ │ │ │ ├── Images.tsx │ │ │ │ ├── Layers.tsx │ │ │ │ ├── Pixabay.tsx │ │ │ │ ├── Templates.tsx │ │ │ │ ├── Text.tsx │ │ │ │ ├── Uploads.tsx │ │ │ │ └── index.tsx │ │ │ ├── PanelListItem.tsx │ │ │ ├── Panels.tsx │ │ │ ├── PanelsList.tsx │ │ │ └── index.tsx │ │ └── Toolbox │ │ │ ├── Toolbox.tsx │ │ │ ├── ToolboxItems │ │ │ ├── Default.tsx │ │ │ ├── Illustration.tsx │ │ │ ├── Image.tsx │ │ │ ├── Locked.tsx │ │ │ ├── MultiElement.tsx │ │ │ ├── Path.tsx │ │ │ ├── Text.tsx │ │ │ ├── components │ │ │ │ ├── Animate.tsx │ │ │ │ ├── Common.tsx │ │ │ │ ├── CopyStyle.tsx │ │ │ │ ├── Delete.tsx │ │ │ │ ├── Duplicate.tsx │ │ │ │ ├── Group.tsx │ │ │ │ ├── Lock.tsx │ │ │ │ ├── Opacity.tsx │ │ │ │ ├── Position.tsx │ │ │ │ └── Spacing.tsx │ │ │ └── index.ts │ │ │ └── index.tsx │ │ └── index.ts ├── services │ ├── api.ts │ ├── iconscout.ts │ └── pixabay.ts ├── setupTests.ts ├── store │ ├── rootReducer.ts │ ├── slices │ │ ├── creations │ │ │ ├── actions.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ ├── elements │ │ │ ├── actions.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ ├── fonts │ │ │ ├── actions.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ ├── templates │ │ │ ├── actions.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ │ └── uploads │ │ │ ├── actions.ts │ │ │ ├── reducer.ts │ │ │ └── selectors.ts │ └── store.ts └── utils │ └── unique.ts ├── tsconfig.json ├── tsconfigExtra.json └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | REACT_APP_PIXABAY_KEY=" 2 | REACT_APP_ICONSCOUT_SECRET="" 3 | REACT_APP_ICONSCOUT_CLIENT_ID="" -------------------------------------------------------------------------------- /.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 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "printWidth": 110, 6 | "arrowParens": "avoid" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 xorb 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Design Editor 2 | 3 | Design editor using React and FabricJS. Create images in React, draw diagrams and arrange compositions using the image editor and save the result to one of several export formats, provides functionality similar to canva.com. 4 | 5 | ![Editor Preview](https://i.ibb.co/thV42bJ/1638452511504.png) 6 | 7 | ## Features 8 | 9 | - [x] Add, remove, resize, reorder, clone, copy/paste objects 10 | - [x] Group/ungroup objects 11 | - [x] Lock/unlock objects 12 | - [x] Object crop support 13 | - [x] Zoom/pan canvas 14 | - [x] Save and Download design 15 | - [x] Context menu 16 | - [x] Animation support, with Fade / Bounce / Shake / Scaling / Rotation / Flash effects 17 | - [x] Interation modes: selection, ctrl + drag grab 18 | - [x] Undo/Redo support 19 | - [x] Guidelines support 20 | - [x] Server side image rendering 21 | 22 | ## How to start 23 | 24 | The following steps shows how to start frontend application. Navigate to `server` directory in order to see how to start it. 25 | 26 | Start in development mode using the following commands. 27 | 28 | ```sh 29 | # install dependencies 30 | yarn install 31 | # start development server 32 | yarn start 33 | ``` 34 | 35 | Web application service will start running at `localhost:3000` 36 | 37 | ## Integrations 38 | 39 | In order to provide rich content, the following integrations are implemented. 40 | 41 | ### Iconscout 42 | 43 | Illusatrions and icons provider. Add credentials to `.env` file. 44 | 45 | ```sh 46 | ICONSCOUT_CLIENT_ID="your-client-id" 47 | ICONSCOUT_SECRET="your-secret" 48 | ``` 49 | 50 | ### Pixabay 51 | 52 | Images provider. Add credentials to `.env` file. 53 | 54 | ```sh 55 | REACT_APP_PIXABAY_KEY="your-key" 56 | ``` 57 | 58 | 59 | ## Join us on Discord 60 | 61 |

62 | 63 | Discord Banner 2 64 | 65 |

66 | 67 | ## Contribution 68 | 69 | Feel free to contribute by opening issues with any questions, bug reports or feature requests. 70 | 71 | ## Author 72 | 73 | Created and maintained by Dany Boza ([db@backium.co](https://twitter.com/xorbmoon)). 74 | 75 | ## License 76 | 77 | [MIT](LICENSE) 78 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const resloveSrc = (...paths) => path.join(__dirname, 'src', ...paths) 4 | 5 | module.exports = { 6 | webpack: { 7 | alias: { 8 | '@': resloveSrc(), 9 | '@assets': resloveSrc('assets'), 10 | '@components': resloveSrc('components'), 11 | '@common': resloveSrc('common'), 12 | '@contexts': resloveSrc('contexts'), 13 | '@hooks': resloveSrc('hooks'), 14 | '@scenes': resloveSrc('scenes'), 15 | '@store': resloveSrc('store'), 16 | '@services': resloveSrc('services'), 17 | '@utils': resloveSrc('utils'), 18 | '@handlers': resloveSrc('handlers'), 19 | }, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scenify-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.6.2", 7 | "@scenify/sdk": "^0.3.2", 8 | "@types/css-font-loading-module": "^0.0.6", 9 | "@types/i18next": "^13.0.0", 10 | "@types/react-router-dom": "^5.1.8", 11 | "@types/styletron-engine-atomic": "^1.1.1", 12 | "@types/styletron-react": "^5.0.3", 13 | "@types/styletron-standard": "^2.0.2", 14 | "axios": "^0.21.1", 15 | "baseui": "^10.1.1", 16 | "craco": "^0.0.3", 17 | "fabric": "^4.5.1", 18 | "nanoid": "^3.1.22", 19 | "react": "^17.0.2", 20 | "react-colorful": "^5.4.0", 21 | "react-custom-scrollbars": "^4.2.1", 22 | "react-dom": "^17.0.2", 23 | "react-hook-form": "^7.17.5", 24 | "react-redux": "^7.2.6", 25 | "react-router-dom": "^5.2.0", 26 | "react-scripts": "4.0.3", 27 | "redux-persist": "^6.0.0", 28 | "resize-observer-polyfill": "^1.5.1", 29 | "styletron-engine-atomic": "^1.4.8", 30 | "styletron-react": "^6.0.1", 31 | "use-debounce": "^7.0.0", 32 | "web-vitals": "^1.0.1" 33 | }, 34 | "scripts": { 35 | "start": "craco start", 36 | "build": "craco build", 37 | "test": "craco test", 38 | "eject": "craco eject", 39 | "format": "prettier --write src" 40 | }, 41 | "eslintConfig": { 42 | "extends": [ 43 | "react-app", 44 | "react-app/jest" 45 | ] 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "@testing-library/jest-dom": "^5.11.4", 61 | "@testing-library/react": "^11.1.0", 62 | "@testing-library/user-event": "^12.1.10", 63 | "@types/fabric": "^4.5.2", 64 | "@types/jest": "^26.0.15", 65 | "@types/node": "^12.0.0", 66 | "@types/react": "^17.0.0", 67 | "@types/react-color": "^3.0.4", 68 | "@types/react-dom": "^17.0.0", 69 | "prettier": "^2.2.1", 70 | "typescript": "^4.1.2" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajpgtech/design-editor/858939f94758a4698a2c21fdaaad296c026f38bf/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 23 | 24 | 25 | 29 | 30 | 31 | 35 | 39 | 40 | 49 | Scenify 50 | 51 | 52 | 56 | 57 | 58 | 59 |
60 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajpgtech/design-editor/858939f94758a4698a2c21fdaaad296c026f38bf/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajpgtech/design-editor/858939f94758a4698a2c21fdaaad296c026f38bf/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 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/Container.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import ResizeObserver from 'resize-observer-polyfill' 3 | import useAppContext from '@hooks/useAppContext' 4 | import Loading from './components/Loading' 5 | import { editorFonts } from './constants/fonts' 6 | import { useAppDispatch } from './store/store' 7 | import { getTemplates } from './store/slices/templates/actions' 8 | import { getUploads } from './store/slices/uploads/actions' 9 | import { getCreations } from './store/slices/creations/actions' 10 | 11 | function Container({ children }) { 12 | const containerRef = useRef() 13 | const { isMobile, setIsMobile } = useAppContext() 14 | const [loaded, setLoaded] = useState(false) 15 | const dispatch = useAppDispatch() 16 | const updateMediaQuery = (value: number) => { 17 | if (!isMobile && value >= 800) { 18 | setIsMobile(false) 19 | } else if (!isMobile && value < 800) { 20 | setIsMobile(true) 21 | } else { 22 | setIsMobile(false) 23 | } 24 | } 25 | useEffect(() => { 26 | const containerElement = containerRef.current 27 | const containerWidth = containerElement.clientWidth 28 | updateMediaQuery(containerWidth) 29 | const resizeObserver = new ResizeObserver(entries => { 30 | const { width = containerWidth } = (entries[0] && entries[0].contentRect) || {} 31 | updateMediaQuery(width) 32 | }) 33 | resizeObserver.observe(containerElement) 34 | return () => { 35 | if (containerElement) { 36 | resizeObserver.unobserve(containerElement) 37 | } 38 | } 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, []) 41 | 42 | useEffect(() => { 43 | loadFonts() 44 | setTimeout(() => { 45 | setLoaded(true) 46 | }, 1000) 47 | }, []) 48 | 49 | const loadFonts = () => { 50 | const promisesList = editorFonts.map(font => { 51 | // @ts-ignore 52 | return new FontFace(font.name, `url(${font.url})`, font.options).load().catch(err => err) 53 | }) 54 | Promise.all(promisesList) 55 | .then(res => { 56 | res.forEach(uniqueFont => { 57 | if (uniqueFont && uniqueFont.family) { 58 | document.fonts.add(uniqueFont) 59 | } 60 | }) 61 | }) 62 | .catch(err => console.log({ err })) 63 | } 64 | 65 | useEffect(() => { 66 | dispatch(getTemplates()) 67 | dispatch(getUploads()) 68 | dispatch(getCreations()) 69 | // eslint-disable-next-line react-hooks/exhaustive-deps 70 | }, []) 71 | 72 | return ( 73 |
82 | {loaded ? <>{children} : } 83 |
84 | ) 85 | } 86 | 87 | export default Container 88 | -------------------------------------------------------------------------------- /src/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Client as Styletron } from 'styletron-engine-atomic' 3 | import { Provider as StyletronProvider } from 'styletron-react' 4 | import { PersistGate } from 'redux-persist/integration/react' 5 | import { LightTheme, BaseProvider } from 'baseui' 6 | import { EditorProvider } from '@scenify/sdk' 7 | import { AppProvider } from './contexts/AppContext' 8 | import store, { persistor } from '@store/store' 9 | import { Provider } from 'react-redux' 10 | 11 | const engine = new Styletron() 12 | 13 | const Providers: FC = ({ children }) => { 14 | return ( 15 | 16 | HEllo} persistor={persistor}> 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default Providers 30 | -------------------------------------------------------------------------------- /src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom' 2 | import Editor from '@scenes/Editor' 3 | import Dashboard from '@scenes/Dashboard' 4 | 5 | const Routes = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default Routes 17 | -------------------------------------------------------------------------------- /src/assets/images/full-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajpgtech/design-editor/858939f94758a4698a2c21fdaaad296c026f38bf/src/assets/images/full-color.png -------------------------------------------------------------------------------- /src/common/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric' 2 | 3 | interface BaseObject { 4 | id: string 5 | name: string 6 | description?: string 7 | } 8 | export type FabricRect = fabric.Rect & BaseObject 9 | export type FabricCircle = fabric.Circle & BaseObject 10 | export type FabricTriangle = fabric.Triangle & BaseObject 11 | export type FabricObject = fabric.Object & BaseObject 12 | export type FabricObjects = FabricObject | FabricRect | FabricCircle | FabricTriangle 13 | 14 | export type FabricRectOptions = fabric.IRectOptions & BaseObject 15 | export type FabricCircleOptions = fabric.ICircleOptions & BaseObject 16 | export type FabricTriangleOptions = fabric.ITriangleOptions & BaseObject 17 | export type FabricObjectOptions = fabric.IObjectOptions & BaseObject 18 | 19 | export type FabricObjectsOptions = FabricObjectOptions | FabricRect | FabricCircle | FabricTriangle 20 | 21 | // CONTEXT TYPES 22 | 23 | export type CanvasType = 'editor' | 'previews' 24 | export type ToolboxType = 'textbox' | 'image' | 'previews' | 'default' 25 | export type ContextMenuType = 'canvas' | 'object' 26 | export interface ContextMenu { 27 | type: ContextMenuType 28 | visible: boolean 29 | top: number 30 | left: number 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Dropzone/DropZone.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | 3 | const DropZone = ({ children, handleDropFiles }: any) => { 4 | const [isDragging, setIsDragging] = useState(false) 5 | let dragCounter = 0 6 | const dropRef = useRef() 7 | 8 | useEffect(() => { 9 | let div = dropRef.current 10 | if (div) { 11 | div.addEventListener('dragenter', handleDragIn) 12 | div.addEventListener('dragleave', handleDragOut) 13 | div.addEventListener('dragover', handleDrag) 14 | div.addEventListener('drop', handleDrop) 15 | } 16 | return () => { 17 | if (div) { 18 | div.removeEventListener('dragenter', handleDragIn) 19 | div.removeEventListener('dragleave', handleDragOut) 20 | div.removeEventListener('dragover', handleDrag) 21 | div.removeEventListener('drop', handleDrop) 22 | } 23 | } 24 | // eslint-disable-next-line react-hooks/exhaustive-deps 25 | }, []) 26 | const handleDrag = e => { 27 | e.preventDefault() 28 | e.stopPropagation() 29 | } 30 | const handleDragIn = e => { 31 | e.preventDefault() 32 | e.stopPropagation() 33 | dragCounter++ 34 | if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { 35 | setIsDragging(true) 36 | } 37 | } 38 | const handleDragOut = e => { 39 | e.preventDefault() 40 | e.stopPropagation() 41 | dragCounter-- 42 | if (dragCounter > 0) return 43 | 44 | setIsDragging(false) 45 | } 46 | const handleDrop = e => { 47 | e.preventDefault() 48 | e.stopPropagation() 49 | setIsDragging(false) 50 | 51 | if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { 52 | handleDropFiles(e.dataTransfer.files) 53 | e.dataTransfer.clearData() 54 | dragCounter = 0 55 | } 56 | } 57 | 58 | return ( 59 |
60 | {isDragging && ( 61 |
75 | Drop files here to upload... 76 |
77 | )} 78 | {children} 79 |
80 | ) 81 | } 82 | 83 | export default DropZone 84 | -------------------------------------------------------------------------------- /src/components/Dropzone/index.ts: -------------------------------------------------------------------------------- 1 | import DropZone from './DropZone' 2 | 3 | export default DropZone 4 | -------------------------------------------------------------------------------- /src/components/Loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | function Loading() { 2 | return ( 3 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ) 24 | } 25 | 26 | export default Loading 27 | -------------------------------------------------------------------------------- /src/components/Loading/index.ts: -------------------------------------------------------------------------------- 1 | import Loading from "./Loading"; 2 | 3 | export default Loading -------------------------------------------------------------------------------- /src/components/icons/Background.tsx: -------------------------------------------------------------------------------- 1 | function Background({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Background 13 | -------------------------------------------------------------------------------- /src/components/icons/Elements.tsx: -------------------------------------------------------------------------------- 1 | function Elements({ size }: { size: number }) { 2 | return ( 3 | 4 | 11 | 12 | ) 13 | } 14 | 15 | export default Elements 16 | -------------------------------------------------------------------------------- /src/components/icons/Illustrations.tsx: -------------------------------------------------------------------------------- 1 | function Illustrations({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Illustrations 13 | -------------------------------------------------------------------------------- /src/components/icons/Images.tsx: -------------------------------------------------------------------------------- 1 | function Images({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Images 13 | -------------------------------------------------------------------------------- /src/components/icons/Logo.tsx: -------------------------------------------------------------------------------- 1 | function Logo({ size }: { size: number }) { 2 | return ( 3 | 4 | 5 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default Logo 33 | -------------------------------------------------------------------------------- /src/components/icons/Pixabay.tsx: -------------------------------------------------------------------------------- 1 | function Pixabay({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 12 | 13 | ) 14 | } 15 | 16 | export default Pixabay 17 | -------------------------------------------------------------------------------- /src/components/icons/Search.tsx: -------------------------------------------------------------------------------- 1 | function Search({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Search 13 | -------------------------------------------------------------------------------- /src/components/icons/Templates.tsx: -------------------------------------------------------------------------------- 1 | function Templates({ size }: { size: number }) { 2 | return ( 3 | 4 | 9 | 10 | ) 11 | } 12 | 13 | export default Templates 14 | -------------------------------------------------------------------------------- /src/components/icons/Text.tsx: -------------------------------------------------------------------------------- 1 | function Text({ size }: { size: number }) { 2 | return ( 3 | 4 | 9 | 10 | ) 11 | } 12 | export default Text 13 | -------------------------------------------------------------------------------- /src/components/icons/Uploads.tsx: -------------------------------------------------------------------------------- 1 | function Uploads({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Uploads 13 | -------------------------------------------------------------------------------- /src/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | import Background from './Background' 2 | import Elements from './Elements' 3 | import Text from './Text' 4 | import Templates from './Templates' 5 | import Search from './Search' 6 | import Images from './Images' 7 | import Illustrations from './Illustrations' 8 | import Pixabay from './Pixabay' 9 | import Uploads from './Uploads' 10 | import Logo from './Logo' 11 | 12 | class Icons { 13 | static Background = Background 14 | static Elements = Elements 15 | static Text = Text 16 | static Templates = Templates 17 | static Search = Search 18 | static Images = Images 19 | static Illustrations = Illustrations 20 | static Pixabay = Pixabay 21 | static Uploads = Uploads 22 | static Logo = Logo 23 | } 24 | 25 | export default Icons 26 | -------------------------------------------------------------------------------- /src/constants/app-options.ts: -------------------------------------------------------------------------------- 1 | export const panelListItems = [ 2 | { 3 | id: 'templates', 4 | name: 'Templates', 5 | }, 6 | { 7 | id: 'elements', 8 | name: 'Elements', 9 | }, 10 | { 11 | id: 'image', 12 | name: 'Images', 13 | }, 14 | { 15 | id: 'uploads', 16 | name: 'Uploads', 17 | }, 18 | { 19 | id: 'text', 20 | name: 'Text', 21 | }, 22 | { 23 | id: 'illustrations', 24 | name: 'Illustrations', 25 | }, 26 | // { 27 | // id: 'background', 28 | // name: 'Background', 29 | // }, 30 | { 31 | id: 'pixabay', 32 | name: 'Pixabay', 33 | }, 34 | ] 35 | 36 | export enum PanelType { 37 | TEMPLATES = 'Templates', 38 | BACKGROUND = 'Background', 39 | } 40 | -------------------------------------------------------------------------------- /src/constants/contants.ts: -------------------------------------------------------------------------------- 1 | export const propertiesToInclude = ['id', 'selectable'] 2 | -------------------------------------------------------------------------------- /src/constants/editor.ts: -------------------------------------------------------------------------------- 1 | export const SecondLevelMenus = ['FontFamily'] 2 | export const FirstLevelMenus = ['Background'] 3 | 4 | export enum SubMenuType { 5 | FONT_FAMILY = 'FontFamily', 6 | BACKGROUND = 'Background', 7 | COLOR = 'Color', 8 | ANIMATIONS = 'Animations', 9 | } 10 | 11 | export const editorFonts = [ 12 | { 13 | name: 'Roboto', 14 | preview: 15 | 'https://font-public.canva.com/YACgEev4gKc/0/thumbnail3539468006015936417.786a5ed94cf61f05978893c9088a06ea.png', 16 | }, 17 | { 18 | name: 'Quicksand', 19 | preview: 20 | 'https://font-public.canva.com/YADWjpfPmdk/0/thumbnail1564020220251902443.bc4d1dede1fc33ec1e227ccfbc723b00.png', 21 | }, 22 | 23 | { 24 | name: 'Josefin Sans', 25 | preview: 26 | 'https://font-public.canva.com/YADWjoIs6hY/0/thumbnail5311368174505054542.97ad47e884b96eb86f0a0f6b9abb0e61.png', 27 | }, 28 | { 29 | name: 'Arimo', 30 | preview: 31 | 'https://font-public.canva.com/YACgEZ1cb1Q/0/thumbnail2753710016227887457.6dfe783f5d32581a0de7b11c49badc89.png', 32 | }, 33 | { 34 | name: 'Open Sans', 35 | preview: 36 | 'https://font-public.canva.com/YAD7QhG2T6o/0/thumbnail4397746033903160930.50a02b6e1eabffa186ed7d1592726fe7.png', 37 | }, 38 | ] 39 | 40 | export const gradients = [ 41 | { 42 | angle: 0, 43 | colors: ['#00c3ff', '#ffff1c'], //linear-gradient(to right, #00c3ff, #ffff1c) 44 | }, 45 | { 46 | angle: 0, 47 | colors: ['#8e2de2', '#4a00e0'], //linear-gradient(to right, #00c3ff, #ffff1c) 48 | }, 49 | { 50 | angle: 0, 51 | colors: ['#c0392b', '#8e44ad'], //linear-gradient(to right, #00c3ff, #ffff1c) 52 | }, 53 | { 54 | angle: 0, 55 | colors: ['#ff00cc', '#333399'], //linear-gradient(to right, #00c3ff, #ffff1c) 56 | }, 57 | { 58 | angle: 0, 59 | colors: ['#ff4b1f', '#1fddff'], //linear-gradient(to right, #00c3ff, #ffff1c) 60 | }, 61 | { 62 | angle: 0, 63 | colors: ['#6a3093', '#a044ff'], //linear-gradient(to right, #00c3ff, #ffff1c) 64 | }, 65 | { 66 | angle: 0, 67 | colors: ['#fc00ff', '#00dbde'], //linear-gradient(to right, #00c3ff, #ffff1c) 68 | }, 69 | { 70 | angle: 0, 71 | colors: ['#ff0084', '#33001b'], //linear-gradient(to right, #00c3ff, #ffff1c) 72 | }, 73 | { 74 | angle: 0, 75 | colors: ['#43cea2', '#185a9d'], //linear-gradient(to right, #00c3ff, #ffff1c) 76 | }, 77 | ] 78 | -------------------------------------------------------------------------------- /src/constants/fonts.ts: -------------------------------------------------------------------------------- 1 | export const editorFonts = [ 2 | { 3 | name: "Uber Move Text", 4 | url:"https://d3q7mfli5umxdg.cloudfront.net/UberMoveTextLight.otf", 5 | options: { style: 'normal', weight: 300 } 6 | }, 7 | { 8 | name: "Uber Move Text", 9 | url:"https://d3q7mfli5umxdg.cloudfront.net/UberMoveTextRegular.otf", 10 | options: { style: 'normal', weight: 400 } 11 | }, 12 | { 13 | name: "Uber Move Text", 14 | url:"https://d3q7mfli5umxdg.cloudfront.net/UberMoveTextMedium.otf", 15 | options: { style: 'normal', weight: 500 } 16 | }, 17 | { 18 | name: "Uber Move Text", 19 | url:"https://d3q7mfli5umxdg.cloudfront.net/UberMoveTextBold.otf", 20 | options: { style: 'normal', weight: 700 } 21 | }, 22 | ] 23 | -------------------------------------------------------------------------------- /src/constants/format-sizes.ts: -------------------------------------------------------------------------------- 1 | const formatSizes = [ 2 | { 3 | id: 1, 4 | name: 'Facebook Image', 5 | description: '1200 x 1200', 6 | size: { 7 | width: 1200, 8 | height: 1200, 9 | }, 10 | }, 11 | { 12 | id: 2, 13 | name: 'Facebook Link', 14 | description: '1200 x 627', 15 | size: { 16 | width: 1200, 17 | height: 627, 18 | }, 19 | }, 20 | { 21 | id: 3, 22 | name: 'Facebook Cover', 23 | description: '820 x 312', 24 | size: { 25 | width: 820, 26 | height: 312, 27 | }, 28 | }, 29 | { 30 | id: 4, 31 | name: 'Facebook Mobile Cover', 32 | description: '640 x 360', 33 | size: { 34 | width: 640, 35 | height: 360, 36 | }, 37 | }, 38 | { 39 | id: 5, 40 | name: 'Facebook Story', 41 | description: '1080 x 1920', 42 | size: { 43 | width: 1080, 44 | height: 1920, 45 | }, 46 | }, 47 | { 48 | id: 6, 49 | name: 'Instagram Story', 50 | description: '1080 x 1920', 51 | size: { 52 | width: 1080, 53 | height: 1920, 54 | }, 55 | }, 56 | { 57 | id: 7, 58 | name: 'Instagram Post', 59 | description: '1080 x 1080', 60 | size: { 61 | width: 1080, 62 | height: 1080, 63 | }, 64 | }, 65 | { 66 | id: 8, 67 | name: 'Twitter Post', 68 | description: '1024 x 521', 69 | size: { 70 | width: 1024, 71 | height: 512, 72 | }, 73 | }, 74 | { 75 | id: 9, 76 | name: 'Twitter Banner', 77 | description: '1500 x 500', 78 | size: { 79 | width: 1500, 80 | height: 500, 81 | }, 82 | }, 83 | { 84 | id: 10, 85 | name: 'Pinterest Post', 86 | description: '736 x 1128', 87 | size: { 88 | width: 736, 89 | height: 1128, 90 | }, 91 | }, 92 | { 93 | id: 11, 94 | name: 'Email Header', 95 | description: '600 x 300', 96 | size: { 97 | width: 600, 98 | height: 300, 99 | }, 100 | }, 101 | { 102 | id: 12, 103 | name: 'Presentation', 104 | description: '1024 x 768', 105 | size: { 106 | width: 1024, 107 | height: 768, 108 | }, 109 | }, 110 | { 111 | id: 13, 112 | name: 'Coupon', 113 | description: '1800 x 750', 114 | size: { 115 | width: 1800, 116 | height: 750, 117 | }, 118 | }, 119 | ] 120 | 121 | export default formatSizes 122 | -------------------------------------------------------------------------------- /src/contexts/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import { PanelType } from '@/constants/app-options' 2 | import { SubMenuType } from '@/constants/editor' 3 | import React, { createContext, useState, FC } from 'react' 4 | 5 | type Template = any 6 | interface IAppContext { 7 | isMobile: boolean | undefined 8 | setIsMobile: React.Dispatch> 9 | templates: Template[] 10 | setTemplates: (templates: Template[]) => void 11 | uploads: any[] 12 | setUploads: (templates: any[]) => void 13 | shapes: any[] 14 | setShapes: (templates: any[]) => void 15 | activePanel: PanelType 16 | setActivePanel: (option: PanelType) => void 17 | activeSubMenu: SubMenuType | null 18 | setActiveSubMenu: (option: SubMenuType) => void 19 | currentTemplate: any 20 | setCurrentTemplate: any 21 | } 22 | 23 | export const AppContext = createContext({ 24 | isMobile: false, 25 | setIsMobile: () => {}, 26 | templates: [], 27 | setTemplates: () => {}, 28 | uploads: [], 29 | setUploads: () => {}, 30 | shapes: [], 31 | setShapes: () => {}, 32 | activePanel: PanelType.TEMPLATES, 33 | setActivePanel: () => {}, 34 | activeSubMenu: null, 35 | setActiveSubMenu: (value: SubMenuType) => {}, 36 | currentTemplate: {}, 37 | setCurrentTemplate: {}, 38 | }) 39 | 40 | export const AppProvider: FC = ({ children }) => { 41 | const [isMobile, setIsMobile] = useState(undefined) 42 | const [templates, setTemplates] = useState([]) 43 | const [uploads, setUploads] = useState([]) 44 | const [shapes, setShapes] = useState([]) 45 | const [activePanel, setActivePanel] = useState(PanelType.TEMPLATES) 46 | const [activeSubMenu, setActiveSubMenu] = useState(null) 47 | const [currentTemplate, setCurrentTemplate] = useState(null) 48 | const context = { 49 | isMobile, 50 | setIsMobile, 51 | templates, 52 | setTemplates, 53 | activePanel, 54 | setActivePanel, 55 | shapes, 56 | setShapes, 57 | activeSubMenu, 58 | setActiveSubMenu, 59 | uploads, 60 | setUploads, 61 | currentTemplate, 62 | setCurrentTemplate, 63 | } 64 | return {children} 65 | } 66 | -------------------------------------------------------------------------------- /src/hooks/useAppContext.tsx: -------------------------------------------------------------------------------- 1 | import { AppContext } from '@/contexts/AppContext' 2 | import { useContext } from 'react' 3 | 4 | function useAppContext() { 5 | const { 6 | isMobile, 7 | setIsMobile, 8 | activePanel, 9 | setActivePanel, 10 | templates, 11 | setTemplates, 12 | shapes, 13 | setShapes, 14 | activeSubMenu, 15 | setActiveSubMenu, 16 | uploads, 17 | setUploads, 18 | currentTemplate, 19 | setCurrentTemplate, 20 | } = useContext(AppContext) 21 | return { 22 | isMobile, 23 | setIsMobile, 24 | activePanel, 25 | setActivePanel, 26 | templates, 27 | setTemplates, 28 | shapes, 29 | setShapes, 30 | activeSubMenu, 31 | setActiveSubMenu, 32 | uploads, 33 | setUploads, 34 | currentTemplate, 35 | setCurrentTemplate, 36 | } 37 | } 38 | 39 | export default useAppContext 40 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import reportWebVitals from './reportWebVitals' 3 | import Providers from './Providers' 4 | import Routes from './Routes' 5 | import Container from './Container' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ) 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals() 20 | -------------------------------------------------------------------------------- /src/interfaces/editor.ts: -------------------------------------------------------------------------------- 1 | export interface Template { 2 | id: string 3 | name: string 4 | frame: Frame 5 | objects: any[] 6 | background: { 7 | type: string 8 | value: string 9 | } 10 | preview: string 11 | } 12 | 13 | interface Frame { 14 | width: number 15 | height: number 16 | } 17 | 18 | interface ShapeBaseOptions { 19 | id: string 20 | name: string 21 | top: number 22 | left: number 23 | angle: number 24 | width: number 25 | height: number 26 | originX: string 27 | originY: string 28 | scaleX: number 29 | scaleY: number 30 | fill: string 31 | } 32 | 33 | interface TextMetadata { 34 | textAlign: string 35 | fontFamily: string 36 | fontSize: number 37 | fontWeight: string 38 | charspacing: number 39 | lineheight: number 40 | text: string 41 | } 42 | 43 | interface ImageMetadata { 44 | value: string 45 | } 46 | 47 | interface ElementMetadata { 48 | value: number[][] 49 | fill: string 50 | preview: string 51 | } 52 | 53 | export interface ShapeTemplate extends ShapeBaseOptions { 54 | metadata: T 55 | } 56 | 57 | export type IText = ShapeTemplate 58 | export type IImage = ShapeTemplate 59 | export type IElement = ShapeTemplate 60 | export type ShapeType = IText | IImage | IElement 61 | 62 | export interface Uploading { 63 | status: string 64 | progress: number 65 | } 66 | export interface IUpload { 67 | id: string 68 | contentType: string 69 | folder: string 70 | name: string 71 | type: string 72 | url: string 73 | } 74 | 75 | type FontVariant = '300' | 'regular' | '400' | '500' | '600' | '700' | '800' 76 | 77 | type FontFile = Record 78 | export interface IFontFamily { 79 | id: string 80 | family: string 81 | variants: FontVariant[] 82 | files: FontFile[] 83 | subsets: string[] 84 | version: string 85 | lastModified: string 86 | category: string 87 | kind: string 88 | } 89 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /src/scenes/Dashboard/Creations.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'baseui/button' 2 | import { Plus } from 'baseui/icon' 3 | import { Scrollbars } from 'react-custom-scrollbars' 4 | import { Link } from 'react-router-dom' 5 | import { useSelector } from 'react-redux' 6 | import { selectCreations } from '@store/slices/creations/selectors' 7 | import { nanoid } from 'nanoid' 8 | 9 | const Header = () => { 10 | return ( 11 |
20 |
My creations
21 | 22 | 23 | 24 |
25 | ) 26 | } 27 | const Searcher = () => { 28 | return ( 29 |
40 |
Start a new creation
{' '} 41 |
42 | ) 43 | } 44 | 45 | const TemplatesList = () => { 46 | const creations = useSelector(selectCreations) 47 | return ( 48 |
49 |
All creations
50 |
51 |
58 | {creations.map(creation => { 59 | return ( 60 | 61 |
62 | creation 67 |
{creation.name}
68 |
69 | 70 | ) 71 | })} 72 |
73 |
74 |
75 | ) 76 | } 77 | 78 | function Creations() { 79 | return ( 80 |
81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 |
89 | ) 90 | } 91 | 92 | export default Creations 93 | -------------------------------------------------------------------------------- /src/scenes/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, Route, NavLink } from 'react-router-dom' 2 | import Icons from '@components/icons' 3 | import { useStyletron } from 'baseui' 4 | import Templates from './Templates' 5 | import Creations from './Creations' 6 | import Uploads from './Uploads' 7 | 8 | function Dashboard() { 9 | const [css] = useStyletron() 10 | const navLink = css({ 11 | padding: '1rem 0 1rem 1rem', 12 | width: '180px', 13 | textAlign: 'center', 14 | textDecoration: 'none', 15 | fontFamily: 'Uber Move Text', 16 | fontWeight: 500, 17 | display: 'flex', 18 | alignItems: 'center', 19 | gap: '1rem', 20 | color: '#000', 21 | }) 22 | 23 | const activeNavLink = css({ 24 | color: 'rgba(255,255,255)', 25 | background: '#000', 26 | }) 27 | return ( 28 |
29 |
30 |
31 | 32 |
33 |
44 | 45 | 46 | Templates 47 | 48 | 49 | 50 | My creations 51 | 52 | 53 | 54 | My uploads 55 | 56 |
57 |
60 | editor 61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 | ) 72 | } 73 | 74 | export default Dashboard 75 | -------------------------------------------------------------------------------- /src/scenes/Dashboard/Templates.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'baseui/input' 2 | import { Button } from 'baseui/button' 3 | import { Search } from 'baseui/icon' 4 | import { Scrollbars } from 'react-custom-scrollbars' 5 | import { Link } from 'react-router-dom' 6 | import { useSelector } from 'react-redux' 7 | import { selectTemplates } from '@/store/slices/templates/selectors' 8 | import { nanoid } from 'nanoid' 9 | 10 | const Header = () => { 11 | return
Templates
12 | } 13 | const Searcher = () => { 14 | return ( 15 |
26 |
It’s a great day to start creating.
27 |
28 | } placeholder="Find templates" />{' '} 29 | 30 |
31 |
32 | ) 33 | } 34 | 35 | const TemplatesList = () => { 36 | const templates = useSelector(selectTemplates) 37 | return ( 38 |
39 |
Explore our templates
40 |
41 |
53 | Popular 54 |
55 |
66 | Recent 67 |
68 |
69 |
70 |
77 | {templates.map(template => { 78 | return ( 79 | 89 |
90 | template 95 | 96 |
{template.name}
97 |
98 | 99 | ) 100 | })} 101 |
102 |
103 |
104 | ) 105 | } 106 | function Templates() { 107 | return ( 108 |
109 | 110 |
111 |
112 | 113 | 114 |
115 |
116 |
117 | ) 118 | } 119 | 120 | export default Templates 121 | -------------------------------------------------------------------------------- /src/scenes/Dashboard/Uploads.tsx: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'react-custom-scrollbars' 2 | import { useSelector } from 'react-redux' 3 | import { selectUploads } from '@/store/slices/uploads/selectors' 4 | 5 | const Header = () => { 6 | return ( 7 |
16 |
My uploads
17 | {/* 18 | 19 | */} 20 |
21 | ) 22 | } 23 | const Searcher = () => { 24 | return ( 25 |
36 |
Manage your uploads
{' '} 37 |
38 | ) 39 | } 40 | 41 | const TemplatesList = () => { 42 | // const { uploads } = useAppContext() 43 | const uploads = useSelector(selectUploads) 44 | return ( 45 |
46 |
All uploads
47 |
48 |
55 | {uploads.map(upload => { 56 | return ( 57 |
58 | template 63 | {/*
{upload.name}
*/} 64 |
65 | ) 66 | })} 67 |
68 |
69 |
70 | ) 71 | } 72 | function Uploads() { 73 | return ( 74 |
75 | 76 |
77 |
78 | 79 | 80 |
81 |
82 |
83 | ) 84 | } 85 | 86 | export default Uploads 87 | -------------------------------------------------------------------------------- /src/scenes/Dashboard/index.ts: -------------------------------------------------------------------------------- 1 | import Dashboard from "./Dashboard"; 2 | 3 | export default Dashboard -------------------------------------------------------------------------------- /src/scenes/Editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import useAppContext from '@/hooks/useAppContext' 3 | import Editor, { useEditor } from '@scenify/sdk' 4 | import { useParams } from 'react-router' 5 | import { getElements } from '@store/slices/elements/actions' 6 | import { getFonts } from '@store/slices/fonts/actions' 7 | import { useAppDispatch } from '@store/store' 8 | import Navbar from './components/Navbar' 9 | import Panels from './components/Panels' 10 | import Toolbox from './components/Toolbox' 11 | import Footer from './components/Footer' 12 | import api from '@services/api' 13 | 14 | function App({ location }) { 15 | const { setCurrentTemplate } = useAppContext() 16 | const editor = useEditor() 17 | const { id } = useParams<{ id: string }>() 18 | const dispath = useAppDispatch() 19 | 20 | useEffect(() => { 21 | dispath(getElements()) 22 | dispath(getFonts()) 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, []) 25 | const editorConfig = { 26 | clipToFrame: true, 27 | scrollLimit: 0, 28 | } 29 | 30 | useEffect(() => { 31 | if (id && editor && location) { 32 | const locationTemplate = location?.state?.template 33 | if (locationTemplate) { 34 | setCurrentTemplate(locationTemplate) 35 | handleLoadTemplate(locationTemplate) 36 | } else { 37 | api.getCreationById(id).then(data => { 38 | if (data && data.object !== 'error') { 39 | setCurrentTemplate(data) 40 | handleLoadTemplate(data) 41 | } 42 | }) 43 | } 44 | } 45 | // eslint-disable-next-line react-hooks/exhaustive-deps 46 | }, [id, editor, location]) 47 | 48 | const handleLoadTemplate = async template => { 49 | const fonts = [] 50 | template.objects.forEach(object => { 51 | if (object.type === 'StaticText' || object.type === 'DynamicText') { 52 | fonts.push({ 53 | name: object.metadata.fontFamily, 54 | url: object.metadata.fontURL, 55 | options: { style: 'normal', weight: 400 }, 56 | }) 57 | } 58 | }) 59 | 60 | const filteredFonts = fonts.filter(f => !!f.url) 61 | if (filteredFonts.length > 0) { 62 | await loadFonts(filteredFonts) 63 | } 64 | 65 | editor.importFromJSON(template) 66 | } 67 | 68 | const loadFonts = fonts => { 69 | const promisesList = fonts.map(font => { 70 | // @ts-ignore 71 | return new FontFace(font.name, `url(${font.url})`, font.options).load().catch(err => err) 72 | }) 73 | return new Promise((resolve, reject) => { 74 | Promise.all(promisesList) 75 | .then(res => { 76 | res.forEach(uniqueFont => { 77 | // @ts-ignore 78 | if (uniqueFont && uniqueFont.family) { 79 | // @ts-ignore 80 | document.fonts.add(uniqueFont) 81 | resolve(true) 82 | } 83 | }) 84 | }) 85 | .catch(err => reject(err)) 86 | }) 87 | } 88 | return ( 89 |
99 | 100 |
101 | 102 |
103 | 104 |
105 | 106 |
107 |
108 |
109 |
110 |
111 | ) 112 | } 113 | 114 | export default App 115 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Button, KIND, SHAPE, SIZE } from 'baseui/button' 2 | import { styled } from 'baseui' 3 | import { Plus, CheckIndeterminate } from 'baseui/icon' 4 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 5 | import { Scrollbars } from 'react-custom-scrollbars' 6 | import { useEditor, useEditorContext } from '@scenify/sdk' 7 | 8 | const Container = styled('div', props => ({ 9 | backgroundColor: '#f6f7f9', 10 | display: 'flex', 11 | position: 'absolute', 12 | bottom: '20px', 13 | right: '20px', 14 | })) 15 | 16 | const zoomValues = [0.27, 0.5, 0.75, 0.92, 1, 1.25, 1.5, 1.75, 2, 3, 4, 5] 17 | 18 | const ZoomItemContainer = styled('div', () => ({ 19 | height: '38px', 20 | display: 'flex', 21 | alignItems: 'center', 22 | fontSize: '0.85rem', 23 | paddingLeft: '1rem', 24 | ':hover': { 25 | backgroundColor: 'rgba(0,0,0,0.075)', 26 | cursor: 'pointer', 27 | }, 28 | })) 29 | function Footer() { 30 | const editor = useEditor() 31 | const { zoomRatio } = useEditorContext() 32 | return ( 33 | 34 |
35 | 38 | ( 42 |
51 | 52 | { 54 | editor.zoomToFit() 55 | close() 56 | }} 57 | > 58 | Fit canvas 59 | 60 | {zoomValues.map(zv => ( 61 | { 63 | editor.zoomToRatio(zv) 64 | close() 65 | }} 66 | key={zv} 67 | > 68 | {Math.round(zv * 100) + '%'} 69 | 70 | ))} 71 | 72 |
73 | )} 74 | > 75 | 95 |
96 | 99 |
100 |
101 | ) 102 | } 103 | 104 | export default Footer 105 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Footer/index.ts: -------------------------------------------------------------------------------- 1 | import Footer from './Footer' 2 | 3 | export default Footer 4 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Background.tsx: -------------------------------------------------------------------------------- 1 | function Background({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Background 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Backward.tsx: -------------------------------------------------------------------------------- 1 | function Backward({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Backward 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Bold.tsx: -------------------------------------------------------------------------------- 1 | function Bold({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Bold 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/ChevronLeft.tsx: -------------------------------------------------------------------------------- 1 | function ChevronLeft({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default ChevronLeft 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/CopyStyle.tsx: -------------------------------------------------------------------------------- 1 | function CopyStyle({ size }: { size: number }) { 2 | return ( 3 | 4 | 10 | 11 | ) 12 | } 13 | 14 | export default CopyStyle 15 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Delete.tsx: -------------------------------------------------------------------------------- 1 | function Delete({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default Delete 15 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Duplicate.tsx: -------------------------------------------------------------------------------- 1 | function Duplicate({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Duplicate 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Elements.tsx: -------------------------------------------------------------------------------- 1 | function Elements({ size }: { size: number }) { 2 | return ( 3 | 4 | 11 | 12 | ) 13 | } 14 | 15 | export default Elements 16 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/FillColor.tsx: -------------------------------------------------------------------------------- 1 | import FullColor from '@assets/images/full-color.png' 2 | 3 | function FillColor({ color = '#000000' }: { size?: number; color: string }) { 4 | return ( 5 |
6 | {color === '#000000' || color === '#ffffff' ? ( 7 | color picker 8 | ) : ( 9 |
10 | )} 11 |
12 | ) 13 | } 14 | 15 | export default FillColor 16 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Forward.tsx: -------------------------------------------------------------------------------- 1 | function Forward({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Forward 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Images.tsx: -------------------------------------------------------------------------------- 1 | function Images({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Images 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Italic.tsx: -------------------------------------------------------------------------------- 1 | function Italic({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Italic 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Locked.tsx: -------------------------------------------------------------------------------- 1 | function Locked({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default Locked 14 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Logo.tsx: -------------------------------------------------------------------------------- 1 | function Logo({ size }: { size: number }) { 2 | return ( 3 | 4 | 5 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default Logo 33 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Opacity..tsx: -------------------------------------------------------------------------------- 1 | function Opacity({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 29 | ) 30 | } 31 | 32 | export default Opacity 33 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Redo.tsx: -------------------------------------------------------------------------------- 1 | function Redo({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Redo 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Spacing.tsx: -------------------------------------------------------------------------------- 1 | function Spacing({ size }: { size: number }) { 2 | return ( 3 | 4 | 10 | 14 | 20 | 21 | ) 22 | } 23 | 24 | export default Spacing 25 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Templates.tsx: -------------------------------------------------------------------------------- 1 | function Templates({ size }: { size: number }) { 2 | return ( 3 | 4 | 9 | 10 | ) 11 | } 12 | 13 | export default Templates 14 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Text.tsx: -------------------------------------------------------------------------------- 1 | function Text({ size }: { size: number }) { 2 | return ( 3 | 4 | 9 | 10 | ) 11 | } 12 | export default Text 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/TextAlignCenter.tsx: -------------------------------------------------------------------------------- 1 | function TextAlignCenter({ size }: { size: number }) { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | export default TextAlignCenter 12 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/TextAlignJustify.tsx: -------------------------------------------------------------------------------- 1 | function TextAlignJustify({ size }: { size: number }) { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default TextAlignJustify 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/TextAlignLeft.tsx: -------------------------------------------------------------------------------- 1 | function TextAlignLeft({ size }: { size: number }) { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default TextAlignLeft 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/TextAlignRight.tsx: -------------------------------------------------------------------------------- 1 | function TextAlignRight({ size }: { size: number }) { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default TextAlignRight 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/TextColor.tsx: -------------------------------------------------------------------------------- 1 | function TextColor({ size, color = '#000000' }: { size: number; color: string }) { 2 | return ( 3 |
4 | 5 | 9 | 10 | {color === '#000000' || color === '#ffffff' ? ( 11 |
12 | color picker 18 |
19 | ) : ( 20 |
23 | )} 24 |
25 | ) 26 | } 27 | 28 | export default TextColor 29 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/TextSpacing.tsx: -------------------------------------------------------------------------------- 1 | function TextSpacing({ size }: { size: number }) { 2 | return ( 3 | 4 | 5 | 6 | 7 | 11 | 12 | ) 13 | } 14 | 15 | export default TextSpacing 16 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/TimeFast.tsx: -------------------------------------------------------------------------------- 1 | function Timefast({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Timefast 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/ToBack.tsx: -------------------------------------------------------------------------------- 1 | function ToBack({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default ToBack 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/ToFront.tsx: -------------------------------------------------------------------------------- 1 | function ToFront({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | export default ToFront 12 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Underline.tsx: -------------------------------------------------------------------------------- 1 | function Underline({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default Underline 14 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Undo.tsx: -------------------------------------------------------------------------------- 1 | function Undo({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Undo 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Unlocked.tsx: -------------------------------------------------------------------------------- 1 | function Elements({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | 10 | ) 11 | } 12 | 13 | export default Elements 14 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/Uploads.tsx: -------------------------------------------------------------------------------- 1 | function Uploads({ size }: { size: number }) { 2 | return ( 3 | 4 | 8 | 9 | ) 10 | } 11 | 12 | export default Uploads 13 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Icons/index.ts: -------------------------------------------------------------------------------- 1 | import Background from './Background' 2 | import Elements from './Elements' 3 | import Text from './Text' 4 | import Templates from './Templates' 5 | import Delete from './Delete' 6 | import Locked from './Locked' 7 | import UnLocked from './Unlocked' 8 | import Bold from './Bold' 9 | import Italic from './Italic' 10 | import TextColor from './TextColor' 11 | import FillColor from './FillColor' 12 | import Images from './Images' 13 | import TextAlignCenter from './TextAlignCenter' 14 | import TextAlignRight from './TextAlignRight' 15 | import TextAlignLeft from './TextAlignLeft' 16 | import TextAlignJustify from './TextAlignJustify' 17 | import Underline from './Underline' 18 | import Opacity from './Opacity.' 19 | import Duplicate from './Duplicate' 20 | import ToFront from './ToFront' 21 | import Forward from './Forward' 22 | import ToBack from './ToBack' 23 | import Backward from './Backward' 24 | import Undo from './Undo' 25 | import Redo from './Redo' 26 | import ChevronLeft from './ChevronLeft' 27 | import Spacing from './Spacing' 28 | import CopyStyle from './CopyStyle' 29 | import TimeFast from './TimeFast' 30 | 31 | class Icons { 32 | static Background = Background 33 | static Elements = Elements 34 | static Text = Text 35 | static Templates = Templates 36 | static Delete = Delete 37 | static Locked = Locked 38 | static UnLocked = UnLocked 39 | static Bold = Bold 40 | static Italic = Italic 41 | static TextColor = TextColor 42 | static FillColor = FillColor 43 | static Images = Images 44 | static TextAlignRight = TextAlignRight 45 | static TextAlignCenter = TextAlignCenter 46 | static TextAlignLeft = TextAlignLeft 47 | static TextAlignJustify = TextAlignJustify 48 | static Underline = Underline 49 | static Opacity = Opacity 50 | static Duplicate = Duplicate 51 | static ToFront = ToFront 52 | static Forward = Forward 53 | static ToBack = ToBack 54 | static Backward = Backward 55 | static Undo = Undo 56 | static Redo = Redo 57 | static ChevronLeft = ChevronLeft 58 | static Spacing = Spacing 59 | static CopyStyle = CopyStyle 60 | static TimeFast = TimeFast 61 | } 62 | 63 | export default Icons 64 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { styled, ThemeProvider, DarkTheme } from 'baseui' 2 | import { Button, KIND } from 'baseui/button' 3 | import { Input } from 'baseui/input' 4 | import Icons from '../Icons' 5 | import { useEditor } from '@scenify/sdk' 6 | import { useEffect, useState } from 'react' 7 | import useAppContext from '@/hooks/useAppContext' 8 | import Resize from './components/Resize' 9 | import PreviewTemplate from './components/PreviewTemplate' 10 | import { useHistory, useParams } from 'react-router-dom' 11 | 12 | import api from '@/services/api' 13 | import { useAppDispatch } from '@/store/store' 14 | import { setCreations, updateCreationsList } from '@/store/slices/creations/actions' 15 | import History from './components/History' 16 | const Container = styled('div', props => ({ 17 | height: '70px', 18 | background: props.$theme.colors.background, 19 | display: 'flex', 20 | padding: '0 0.5rem', 21 | justifyContent: 'space-between', 22 | alignItems: 'center', 23 | })) 24 | 25 | function useParamId() { 26 | const params: { id: string | undefined } = useParams() 27 | const [id, setId] = useState('') 28 | useEffect(() => { 29 | const id = params.id ? params.id : '' 30 | setId(id) 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, [params]) 33 | return id 34 | } 35 | 36 | function NavbarEditor() { 37 | const editor = useEditor() 38 | const { currentTemplate } = useAppContext() 39 | const history = useHistory() 40 | const [name, setName] = useState('Untitled design') 41 | const id = useParamId() 42 | const dispatch = useAppDispatch() 43 | const actionText = id ? 'Update' : 'Create' 44 | const [saving, setSaving] = useState(false) 45 | 46 | const handleSave = async () => { 47 | if (editor) { 48 | setSaving(true) 49 | if (id) { 50 | const exportedTemplate = editor.exportToJSON() 51 | const savedTemplate = await api.updateCreation(id, { ...exportedTemplate, name }) 52 | dispatch(updateCreationsList(savedTemplate)) 53 | } else { 54 | const exportedTemplate = editor.exportToJSON() 55 | const savedTemplate = await api.createCreation({ ...exportedTemplate, name }) 56 | dispatch(setCreations([savedTemplate])) 57 | history.push(`/edit/${savedTemplate.id}`) 58 | } 59 | setSaving(false) 60 | } 61 | } 62 | 63 | const handleSaveAsTemplate = async () => { 64 | const exportedTemplate = editor.exportToJSON() 65 | const savedTemplate = await api.createTemplate(exportedTemplate) 66 | console.log({ savedTemplate }) 67 | } 68 | 69 | useEffect(() => { 70 | if (currentTemplate) { 71 | setName(currentTemplate.name) 72 | } 73 | }, [currentTemplate]) 74 | 75 | return ( 76 | 77 | 78 |
79 | 87 | 88 | 89 |
90 |
91 |
92 | setName(e.target.value)} 95 | overrides={{ 96 | Root: { 97 | style: { 98 | borderTopStyle: 'none', 99 | borderBottomStyle: 'none', 100 | borderRightStyle: 'none', 101 | borderLeftStyle: 'none', 102 | backgroundColor: 'rgba(255,255,255,0)', 103 | }, 104 | }, 105 | InputContainer: { 106 | style: { 107 | backgroundColor: 'rgba(255,255,255,0)', 108 | }, 109 | }, 110 | }} 111 | /> 112 |
113 |
114 |
115 | 118 | 121 | 122 |
123 |
124 |
125 | ) 126 | } 127 | 128 | export default NavbarEditor 129 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Navbar/components/File.tsx: -------------------------------------------------------------------------------- 1 | import { Button, KIND } from 'baseui/button' 2 | import { styled, ThemeProvider, LightTheme } from 'baseui' 3 | import { ListItem, ListItemLabel } from 'baseui/list' 4 | 5 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 6 | 7 | const Container = styled('div', props => ({ 8 | background: props.$theme.colors.background, 9 | color: props.$theme.colors.primary, 10 | width: '240px', 11 | fontFamily: 'Uber Move Text', 12 | padding: '1rem 1rem', 13 | })) 14 | export default function Resize() { 15 | return ( 16 | ( 20 | 21 | 22 | 23 | Facebook cover 24 | 25 | 26 | Facebook add 27 | 28 | 29 | Facebook post 30 | 31 | 32 | 33 | )} 34 | > 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Navbar/components/History.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from 'baseui' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | 4 | import { useEditor } from '@scenify/sdk' 5 | import Icons from '../../Icons' 6 | import { useEffect, useState } from 'react' 7 | 8 | const Container = styled('div', props => ({ 9 | display: 'flex', 10 | alignItems: 'center', 11 | })) 12 | 13 | function History() { 14 | const editor = useEditor() 15 | 16 | const [historyStatus, setHistoryStatus] = useState({ hasUndo: false, hasRedo: false }) 17 | useEffect(() => { 18 | const handleHistoryChange = (data: any) => { 19 | setHistoryStatus({ ...historyStatus, hasUndo: data.hasUndo, hasRedo: data.hasRedo }) 20 | } 21 | if (editor) { 22 | editor.on('history:changed', handleHistoryChange) 23 | } 24 | return () => { 25 | if (editor) { 26 | editor.off('history:changed', handleHistoryChange) 27 | } 28 | } 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | }, [editor]) 31 | 32 | return ( 33 | 34 | 48 | 62 | 63 | ) 64 | } 65 | export default History 66 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Navbar/components/PreviewTemplate.tsx: -------------------------------------------------------------------------------- 1 | import { Button, KIND } from 'baseui/button' 2 | import { Modal, ModalBody, SIZE } from 'baseui/modal' 3 | import { useEffect, useState } from 'react' 4 | import { ThemeProvider, LightTheme } from 'baseui' 5 | import { flatten, uniq } from 'lodash' 6 | import { useEditor } from '@scenify/sdk' 7 | import { FormControl } from 'baseui/form-control' 8 | import { Input } from 'baseui/input' 9 | 10 | function PreviewTemplate() { 11 | const [isOpen, setIsOpen] = useState(false) 12 | const editor = useEditor() 13 | const [options, setOptions] = useState({}) 14 | const [previewImage, setPreviewImage] = useState(null) 15 | 16 | useEffect(() => { 17 | if (isOpen && editor) { 18 | const template = editor.exportToJSON() 19 | const keys = template.objects.map(object => { 20 | return object.metadata && object.metadata.keys ? object.metadata.keys : [] 21 | }) 22 | 23 | const params: Record = {} 24 | const uniqElements = uniq(flatten(keys)) 25 | uniqElements.forEach(key => { 26 | params[key] = '' 27 | }) 28 | 29 | setOptions(params) 30 | if (uniqElements.length === 0) { 31 | handleBuildImage() 32 | } 33 | } 34 | // eslint-disable-next-line react-hooks/exhaustive-deps 35 | }, [isOpen, editor]) 36 | 37 | const handleBuildImage = async () => { 38 | // @ts-ignore 39 | const image = await editor.toPNG(options) 40 | setPreviewImage(image) 41 | } 42 | 43 | const close = () => { 44 | setIsOpen(false) 45 | setPreviewImage(null) 46 | setOptions({}) 47 | } 48 | 49 | const handleDownloadImage = () => { 50 | if (editor) { 51 | if (previewImage) { 52 | const a = document.createElement('a') 53 | a.href = previewImage 54 | a.download = 'drawing.png' 55 | a.click() 56 | } 57 | } 58 | } 59 | 60 | return ( 61 | <> 62 | 65 | 66 | 83 | 84 |
85 | {previewImage ? ( 86 |
89 |
90 | Preview Image 91 |
92 | preview 93 | 94 |
95 | ) : ( 96 |
97 |
Params
98 | {Object.keys(options).map(option => { 99 | return ( 100 | 101 | setOptions({ ...options, [option]: e.target.value })} 105 | /> 106 | 107 | ) 108 | })} 109 | 110 |
111 | )} 112 |
113 |
114 |
115 |
116 | 117 | ) 118 | } 119 | 120 | export default PreviewTemplate 121 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Navbar/components/Resize.tsx: -------------------------------------------------------------------------------- 1 | import { Button, KIND } from 'baseui/button' 2 | import { styled, ThemeProvider, LightTheme } from 'baseui' 3 | import { Select, Value } from 'baseui/select' 4 | import { Input } from 'baseui/input' 5 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 6 | import { useEffect, useState } from 'react' 7 | import formatSizes from '@/constants/format-sizes' 8 | import { useEditorContext, useEditor } from '@scenify/sdk' 9 | 10 | const getLabel = ({ option }: any) => { 11 | return ( 12 |
13 |
{option.name}
14 |
{option.description}
15 |
16 | ) 17 | } 18 | 19 | const Container = styled('div', props => ({ 20 | background: props.$theme.colors.background, 21 | color: props.$theme.colors.primary, 22 | width: '320px', 23 | fontFamily: 'Uber Move Text', 24 | padding: '2rem 2rem', 25 | })) 26 | 27 | export default function Resize() { 28 | const [value, setValue] = useState([]) 29 | const [customSize, setCustomSize] = useState({ width: 0, height: 0 }) 30 | const { frameSize } = useEditorContext() as any 31 | 32 | const editor = useEditor() 33 | const updateFormatSize = value => { 34 | setValue(value) 35 | const [frame] = value 36 | editor.frame.setSize(frame.size) 37 | } 38 | const applyCustomSize = () => { 39 | if (customSize.width && customSize.height) { 40 | editor.frame.update(customSize) 41 | } 42 | } 43 | useEffect(() => { 44 | if (frameSize) { 45 | setCustomSize(frameSize) 46 | } 47 | }, [frameSize]) 48 | 49 | return ( 50 | ( 54 | 55 | 56 |
57 |
Size templates
58 | setCustomSize({ ...customSize, width: (e.target as any).value })} 75 | startEnhancer="W" 76 | placeholder="width" 77 | /> 78 | setCustomSize({ ...customSize, height: (e.target as any).value })} 81 | startEnhancer="H" 82 | placeholder="width" 83 | /> 84 |
85 | 86 | 87 |
88 |
89 | )} 90 | > 91 | 92 |
93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Navbar/index.ts: -------------------------------------------------------------------------------- 1 | import Navbar from './Navbar' 2 | export default Navbar 3 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItem.tsx: -------------------------------------------------------------------------------- 1 | import { SubMenuType } from '@/constants/editor' 2 | import useAppContext from '@/hooks/useAppContext' 3 | import { useEditorContext } from '@scenify/sdk' 4 | import { styled } from 'baseui' 5 | import { useEffect } from 'react' 6 | import PanelItems from './PanelItems' 7 | 8 | const Container = styled('div', props => ({ 9 | background: '#ffffff', 10 | width: '360px', 11 | flex: 'none', 12 | boxShadow: '1px 0px 1px rgba(0, 0, 0, 0.15)', 13 | })) 14 | 15 | function PanelsList() { 16 | const { activePanel, activeSubMenu, setActiveSubMenu } = useAppContext() 17 | const { activeObject } = useEditorContext() 18 | 19 | useEffect(() => { 20 | setActiveSubMenu(null) 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | }, [activeObject]) 23 | 24 | const Component = 25 | (activeObject && activeSubMenu) || (!activeObject && activeSubMenu === SubMenuType.COLOR) 26 | ? PanelItems[activeSubMenu] 27 | : PanelItems[activePanel] 28 | 29 | return {Component && } 30 | } 31 | 32 | export default PanelsList 33 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Animations.tsx: -------------------------------------------------------------------------------- 1 | import { StatefulTabs, Tab } from 'baseui/tabs' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { useEditor } from '@scenify/sdk' 4 | 5 | const animations = [ 6 | { 7 | type: 'NONE', 8 | name: 'None', 9 | preview: ' ', 10 | }, 11 | { 12 | type: 'STOMP', 13 | name: 'Stomp', 14 | preview: ' ', 15 | }, 16 | { 17 | type: 'TUMBLE', 18 | name: 'Tumble', 19 | preview: ' ', 20 | }, 21 | { 22 | type: 'RISE', 23 | name: 'Rise', 24 | preview: ' ', 25 | }, 26 | { 27 | type: 'PAN', 28 | name: 'Pan', 29 | preview: ' ', 30 | }, 31 | { 32 | type: 'FADE', 33 | name: 'Fade', 34 | preview: ' ', 35 | }, 36 | { 37 | type: 'BREATHE', 38 | name: 'Breathe', 39 | preview: ' ', 40 | }, 41 | ] 42 | 43 | function Panel() { 44 | const editor = useEditor() 45 | 46 | return ( 47 |
48 | 49 | 65 | Page animations 66 | 67 |
74 | {animations.map(animation => { 75 | return ( 76 |
editor.animate(animation.type)} 79 | style={{ 80 | alignItems: 'center', 81 | cursor: 'pointer', 82 | padding: '10px', 83 | display: 'flex', 84 | justifyContent: 'center', 85 | flexDirection: 'column', 86 | fontSize: '12px', 87 | }} 88 | > 89 | preview 95 |
{animation.name}
96 |
97 | ) 98 | })} 99 |
100 |
101 | {/* Tab 3 content */} 102 |
103 |
104 |
105 | ) 106 | } 107 | 108 | export default Panel 109 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Background.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useEditor } from '@scenify/sdk' 6 | import { HexColorPicker } from 'react-colorful' 7 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 8 | import { Plus } from 'baseui/icon' 9 | import throttle from 'lodash/throttle' 10 | 11 | const colors = [ 12 | '#f19066', 13 | '#f5cd79', 14 | '#546de5', 15 | '#e15f41', 16 | '#c44569', 17 | '#574b90', 18 | '#f78fb3', 19 | '#3dc1d3', 20 | '#e66767', 21 | '#303952', 22 | ] 23 | function Background() { 24 | const [color, setColor] = useState('#b32aa9') 25 | const [value, setValue] = useState('') 26 | const editor = useEditor() 27 | const updateBackgrounColor = throttle((color: string) => { 28 | // handlers.frameHandler.setBackgroundColor(color) 29 | editor.background.setBackgroundColor(color) 30 | setColor(color) 31 | }, 100) 32 | 33 | return ( 34 |
35 |
36 | } 38 | value={value} 39 | onChange={e => setValue((e.target as any).value)} 40 | placeholder="Search background" 41 | clearOnEscape 42 | /> 43 |
44 |
45 | 46 |
47 |
Document colors
48 | 62 | 63 | setValue((e.target as any).value)} 67 | placeholder="#000000" 68 | clearOnEscape 69 | /> 70 |
71 | } 72 | accessibilityType={'tooltip'} 73 | > 74 |
75 |
88 |
99 | 100 |
101 |
102 |
103 | 104 |
105 |
106 |
Default colors
107 |
108 | 109 |
117 | {colors.map(color => ( 118 |
editor.background.setBackgroundColor(color)} 120 | key={color} 121 | style={{ height: '42px', background: color, borderRadius: '4px', cursor: 'pointer' }} 122 | >
123 | ))} 124 |
125 | 126 |
127 | 128 | ) 129 | } 130 | 131 | export default Background 132 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Color.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useActiveObject, useEditor } from '@scenify/sdk' 6 | import { HexColorPicker } from 'react-colorful' 7 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 8 | import { Plus } from 'baseui/icon' 9 | import throttle from 'lodash/throttle' 10 | import { gradients } from '@/constants/editor' 11 | 12 | const colors = [ 13 | '#f19066', 14 | '#f5cd79', 15 | '#546de5', 16 | '#e15f41', 17 | '#c44569', 18 | '#574b90', 19 | '#f78fb3', 20 | '#3dc1d3', 21 | '#e66767', 22 | '#303952', 23 | ] 24 | function Color() { 25 | const [color, setColor] = useState('#b32aa9') 26 | const [value, setValue] = useState('') 27 | const editor = useEditor() 28 | const activeObject = useActiveObject() 29 | 30 | const updateObjectFill = throttle((color: string) => { 31 | if (activeObject) { 32 | editor.update({ fill: color }) 33 | } else { 34 | editor.background.setBackgroundColor(color) 35 | } 36 | setColor(color) 37 | }, 100) 38 | 39 | const updateObjectGradient = throttle((gradient: any) => { 40 | if (activeObject) { 41 | editor.setGradient(gradient) 42 | } else { 43 | editor.background.setGradient(gradient) 44 | } 45 | setColor(color) 46 | }, 100) 47 | 48 | return ( 49 |
50 |
51 | } 53 | value={value} 54 | onChange={e => setValue((e.target as any).value)} 55 | placeholder="Search color" 56 | clearOnEscape 57 | /> 58 |
59 |
60 | 61 |
62 |
Document colors
63 | 77 | 78 | setValue((e.target as any).value)} 82 | placeholder="#000000" 83 | clearOnEscape 84 | /> 85 |
86 | } 87 | accessibilityType={'tooltip'} 88 | > 89 |
90 |
103 |
114 | 115 |
116 |
117 |
118 | 119 |
120 |
121 |
Basic
122 |
123 | 124 |
132 | {colors.map(color => ( 133 |
updateObjectFill(color)} 135 | key={color} 136 | style={{ height: '42px', background: color, borderRadius: '4px', cursor: 'pointer' }} 137 | >
138 | ))} 139 |
140 | 141 |
142 |
Gradient
143 |
144 | 145 |
153 | {gradients.map((gradient, index) => ( 154 |
updateObjectGradient(gradient)} 156 | key={index} 157 | style={{ 158 | height: '42px', 159 | background: `linear-gradient(to right, ${gradient.colors[0]}, ${gradient.colors[1]})`, 160 | borderRadius: '4px', 161 | cursor: 'pointer', 162 | }} 163 | >
164 | ))} 165 |
166 | 167 |
168 | 169 | ) 170 | } 171 | 172 | export default Color 173 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Elements.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'baseui/input' 2 | import Icons from '@components/icons' 3 | import { Scrollbars } from 'react-custom-scrollbars' 4 | import { useState } from 'react' 5 | import { useEditor } from '@scenify/sdk' 6 | import { useSelector } from 'react-redux' 7 | import { selectElements } from '@/store/slices/elements/selectors' 8 | 9 | function Panel() { 10 | const [value, setValue] = useState('') 11 | const elements = useSelector(selectElements) 12 | const editor = useEditor() 13 | return ( 14 |
15 |
16 | } 18 | value={value} 19 | onChange={e => setValue((e.target as any).value)} 20 | placeholder="Search elements" 21 | clearOnEscape 22 | /> 23 |
24 |
25 | 26 |
29 | {elements.map(element => ( 30 |
editor.add(element)} 39 | > 40 | preview 46 |
47 | ))} 48 |
49 |
50 |
51 |
52 | ) 53 | } 54 | 55 | export default Panel 56 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/FontFamily.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useEditor } from '@scenify/sdk' 6 | import { styled } from 'baseui' 7 | import { useSelector } from 'react-redux' 8 | import { selectFonts } from '@/store/slices/fonts/selectors' 9 | import { IFontFamily } from '@/interfaces/editor' 10 | 11 | function FontFamily() { 12 | const [value, setValue] = useState('') 13 | const fonts = useSelector(selectFonts) 14 | const editor = useEditor() 15 | const handleFontFamilyChange = async (fontFamily: IFontFamily) => { 16 | if (editor) { 17 | const fontFile = fontFamily.files['regular' as any] 18 | const font = { 19 | name: fontFamily.family, 20 | url: fontFile, 21 | options: { style: 'normal', weight: 400 }, 22 | } 23 | // @ts-ignore 24 | const fontFace = new FontFace(font.name, `url(${font.url})`, font.options) 25 | fontFace 26 | .load() 27 | .then(loadedFont => { 28 | document.fonts.add(loadedFont) 29 | fontFace.loaded.then(() => { 30 | editor.update({ 31 | fontFamily: fontFamily.family, 32 | metadata: { 33 | fontURL: font.url, 34 | }, 35 | }) 36 | }) 37 | }) 38 | .catch(err => console.log(err)) 39 | } 40 | } 41 | 42 | return ( 43 |
44 |
45 | } 47 | value={value} 48 | onChange={e => setValue((e.target as any).value)} 49 | placeholder="Search font" 50 | clearOnEscape 51 | /> 52 |
53 |
54 | 55 |
56 | {fonts.map(font => ( 57 | handleFontFamilyChange(font)} key={font.id}> 58 | {font.family} 59 | 60 | ))} 61 |
62 |
63 |
64 |
65 | ) 66 | } 67 | 68 | const FontItem = styled('div', props => ({ 69 | cursor: 'pointer', 70 | padding: '14px 5px 14px 5px', 71 | ':hover': { 72 | background: 'rgba(0,0,0,0.045)', 73 | }, 74 | })) 75 | 76 | export default FontFamily 77 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Illustrations.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useEditor } from '@scenify/sdk' 6 | import { useDebounce } from 'use-debounce' 7 | import { getImage, getImages } from '@/services/iconscout' 8 | 9 | function Illustrations() { 10 | const [search, setSearch] = useState('') 11 | const [objects, setObjects] = useState([]) 12 | const [value] = useDebounce(search, 1000) 13 | 14 | const editor = useEditor() 15 | useEffect(() => { 16 | getImages('people') 17 | .then((data: any) => setObjects(data)) 18 | .catch(console.log) 19 | }, []) 20 | 21 | useEffect(() => { 22 | if (value) { 23 | getImages(value) 24 | .then((data: any) => setObjects(data)) 25 | .catch(console.log) 26 | } 27 | }, [value]) 28 | const downloadImage = uuid => { 29 | getImage(uuid) 30 | .then(url => { 31 | const options = { 32 | type: 'StaticVector', 33 | metadata: { src: url }, 34 | } 35 | editor.add(options) 36 | }) 37 | .catch(console.log) 38 | } 39 | 40 | return ( 41 |
42 |
43 | } 45 | value={search} 46 | onChange={e => setSearch((e.target as any).value)} 47 | placeholder="Search illustrations" 48 | clearOnEscape 49 | /> 50 |
51 |
52 | 53 |
56 | {objects.map(obj => ( 57 |
downloadImage(obj.uuid)} 66 | > 67 | svg object 68 |
69 | ))} 70 |
71 |
72 |
73 |
74 | ) 75 | } 76 | 77 | export default Illustrations 78 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Images.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useEditor } from '@scenify/sdk' 6 | 7 | function Images() { 8 | const [search, setSearch] = useState('') 9 | 10 | const editor = useEditor() 11 | 12 | const addDynamicImage = useCallback(() => { 13 | if (editor) { 14 | const objectOptions = { 15 | width: 100, 16 | height: 100, 17 | backgroundColor: '#bdc3c7', 18 | type: 'DynamicImage', 19 | 20 | metadata: { 21 | keyValues: [{ key: '{{image}}', value: '' }], 22 | }, 23 | } 24 | editor.add(objectOptions) 25 | } 26 | }, [editor]) 27 | 28 | return ( 29 |
30 |
31 | } 33 | value={search} 34 | onChange={e => setSearch((e.target as any).value)} 35 | placeholder="Search images" 36 | clearOnEscape 37 | /> 38 |
39 |
40 | 41 |
42 |
54 | Add dynamic image 55 |
56 |
57 |
58 |
59 |
60 | ) 61 | } 62 | 63 | export default Images 64 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Layers.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | 3 | function Panel() { 4 | return Layers 5 | } 6 | 7 | export default Panel 8 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Pixabay.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useEditor } from '@scenify/sdk' 6 | import { getPixabayImages, PixabayImage } from '@/services/pixabay' 7 | import { useDebounce } from 'use-debounce' 8 | 9 | function Images() { 10 | const [search, setSearch] = useState('') 11 | const [images, setImages] = useState([]) 12 | const [value] = useDebounce(search, 1000) 13 | 14 | const editor = useEditor() 15 | useEffect(() => { 16 | getPixabayImages('people') 17 | .then(data => setImages(data)) 18 | .catch(console.log) 19 | }, []) 20 | 21 | useEffect(() => { 22 | if (value) { 23 | getPixabayImages(value) 24 | .then((data: any) => setImages(data)) 25 | .catch(console.log) 26 | } 27 | }, [value]) 28 | 29 | const addImageToCanvas = url => { 30 | const options = { 31 | type: 'StaticImage', 32 | metadata: { src: url }, 33 | } 34 | editor.add(options) 35 | } 36 | 37 | return ( 38 |
39 |
40 | } 42 | value={search} 43 | onChange={e => setSearch((e.target as any).value)} 44 | placeholder="Search images" 45 | clearOnEscape 46 | /> 47 |
48 |
49 | 50 |
53 | {images.map(img => ( 54 |
addImageToCanvas(img.webformatURL)} 62 | > 63 | preview 64 |
65 | ))} 66 |
67 |
68 |
69 |
70 | ) 71 | } 72 | 73 | export default Images 74 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Templates.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useEditor } from '@scenify/sdk' 6 | import { useSelector } from 'react-redux' 7 | import { selectTemplates } from '@/store/slices/templates/selectors' 8 | 9 | function Templates() { 10 | const templates = useSelector(selectTemplates) 11 | const [value, setValue] = useState('') 12 | const editor = useEditor() 13 | 14 | const handleLoadTemplate = async template => { 15 | console.log('LOAD TEMPLATE') 16 | const fonts = [] 17 | template.objects.forEach(object => { 18 | if (object.type === 'StaticText' || object.type === 'DynamicText') { 19 | fonts.push({ 20 | name: object.metadata.fontFamily, 21 | url: object.metadata.fontURL, 22 | options: { style: 'normal', weight: 400 }, 23 | }) 24 | } 25 | }) 26 | const filteredFonts = fonts.filter(f => !!f.url) 27 | if (filteredFonts.length > 0) { 28 | await loadFonts(filteredFonts) 29 | } 30 | editor.importFromJSON(template) 31 | } 32 | 33 | const loadFonts = fonts => { 34 | const promisesList = fonts.map(font => { 35 | // @ts-ignore 36 | return new FontFace(font.name, `url(${font.url})`, font.options).load().catch(err => err) 37 | }) 38 | return new Promise((resolve, reject) => { 39 | Promise.all(promisesList) 40 | .then(res => { 41 | res.forEach(uniqueFont => { 42 | // @ts-ignore 43 | if (uniqueFont && uniqueFont.family) { 44 | // @ts-ignore 45 | document.fonts.add(uniqueFont) 46 | resolve(true) 47 | } 48 | }) 49 | }) 50 | .catch(err => reject(err)) 51 | }) 52 | } 53 | 54 | return ( 55 |
56 |
57 | } 59 | value={value} 60 | onChange={e => setValue((e.target as any).value)} 61 | placeholder="Search templates" 62 | clearOnEscape 63 | /> 64 |
65 |
66 | 67 |
70 | {templates.map(template => ( 71 |
handleLoadTemplate(template)} 80 | > 81 | preview 82 |
83 | ))} 84 |
85 |
86 |
87 |
88 | ) 89 | } 90 | 91 | export default Templates 92 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Text.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor } from '@scenify/sdk' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { Input } from 'baseui/input' 4 | import Icons from '@components/icons' 5 | import { useState } from 'react' 6 | 7 | function Panel() { 8 | const [value, setValue] = useState('') 9 | 10 | const editor = useEditor() 11 | 12 | const addStaticText = () => { 13 | const options = { 14 | type: 'StaticText', 15 | width: 320, 16 | metadata: { 17 | text: 'Add static text', 18 | fontSize: 32, 19 | fontWeight: 500, 20 | fontFamily: 'Amiko', 21 | textAlign: 'center', 22 | fontURL: 'https://fonts.gstatic.com/s/amiko/v5/WwkQxPq1DFK04tqlc17MMZgJ.ttf', 23 | }, 24 | } 25 | editor.add(options) 26 | } 27 | 28 | const addDynamicText = () => { 29 | const options = { 30 | type: 'DynamicText', 31 | width: 320, 32 | metadata: { 33 | text: 'Add dynamic text', 34 | fontSize: 32, 35 | fontWeight: 500, 36 | fontFamily: 'Lexend', 37 | textAlign: 'center', 38 | }, 39 | } 40 | editor.add(options) 41 | } 42 | 43 | return ( 44 |
45 |
46 | } 48 | value={value} 49 | onChange={e => setValue((e.target as any).value)} 50 | placeholder="Search text" 51 | clearOnEscape 52 | /> 53 |
54 |
55 | 56 |
64 |
75 | Add static text 76 |
77 |
88 | Add dynamic text 89 |
90 |
91 |
92 |
93 |
94 | ) 95 | } 96 | 97 | export default Panel 98 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/Uploads.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | import { Scrollbars } from 'react-custom-scrollbars' 3 | import { useEditor } from '@scenify/sdk' 4 | import DropZone from '@components/Dropzone' 5 | 6 | import { uniqueFilename } from '@/utils/unique' 7 | import { useSelector } from 'react-redux' 8 | import { selectUploading, selectUploads } from '@/store/slices/uploads/selectors' 9 | import { useAppDispatch } from '@/store/store' 10 | import { setUploading, uploadFile } from '@/store/slices/uploads/actions' 11 | 12 | function Uploads() { 13 | const [currentFile, setCurrentFile] = useState(null) 14 | const inputFileRef = useRef(null) 15 | const uploads = useSelector(selectUploads) 16 | const uploading = useSelector(selectUploading) 17 | const editor = useEditor() 18 | const dispatch = useAppDispatch() 19 | const handleDropFiles = (files: FileList) => { 20 | const file = files[0] 21 | handleUploadFile(file) 22 | const reader = new FileReader() 23 | reader.addEventListener( 24 | 'load', 25 | function () { 26 | setCurrentFile(reader.result) 27 | }, 28 | false 29 | ) 30 | 31 | if (file) { 32 | reader.readAsDataURL(file) 33 | } 34 | } 35 | 36 | const handleUploadFile = async (file: File) => { 37 | try { 38 | const updatedFileName = uniqueFilename(file.name) 39 | const updatedFile = new File([file], updatedFileName) 40 | dispatch( 41 | setUploading({ 42 | progress: 0, 43 | status: 'IN_PROGRESS', 44 | }) 45 | ) 46 | dispatch(uploadFile({ file: updatedFile })) 47 | // const response = await api.getSignedURLForUpload({ name: updatedFileName }) 48 | // await axios.put(response.url, updatedFile, { 49 | // headers: { 'Content-Type': 'image/png' }, 50 | // }) 51 | // await api.updateUploadFile({ name: updatedFileName }) 52 | } catch (err) { 53 | console.log({ err }) 54 | } 55 | } 56 | 57 | const addImageToCanvas = url => { 58 | const options = { 59 | type: 'StaticImage', 60 | metadata: { src: url }, 61 | } 62 | editor.add(options) 63 | } 64 | 65 | const handleFileInput = (e: React.ChangeEvent) => { 66 | handleDropFiles(e.target.files) 67 | } 68 | 69 | const handleInputFileRefClick = () => { 70 | inputFileRef.current?.click() 71 | } 72 | 73 | return ( 74 | 75 |
76 |
77 |
90 | Upload file 91 |
92 | 99 |
100 |
101 | 102 |
110 | {uploading && uploaded} 111 | 112 | {uploads.map(upload => ( 113 |
addImageToCanvas(upload.url)} 121 | > 122 |
123 | preview 124 |
125 |
126 | ))} 127 |
128 |
129 |
130 |
131 |
132 | ) 133 | } 134 | 135 | export default Uploads 136 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelItems/index.tsx: -------------------------------------------------------------------------------- 1 | import Background from './Background' 2 | import Templates from './Templates' 3 | import Layers from './Layers' 4 | import Text from './Text' 5 | import Elements from './Elements' 6 | import FontFamily from './FontFamily' 7 | import Color from './Color' 8 | import Images from './Images' 9 | import Illustrations from './Illustrations' 10 | import Pixabay from './Pixabay' 11 | import Uploads from './Uploads' 12 | import Animations from './Animations' 13 | class PanelItems { 14 | static Background = Background 15 | static Text = Text 16 | static Layers = Layers 17 | static Templates = Templates 18 | static Elements = Elements 19 | static FontFamily = FontFamily 20 | static Color = Color 21 | static Images = Images 22 | static Illustrations = Illustrations 23 | static Pixabay = Pixabay 24 | static Uploads = Uploads 25 | static Animations = Animations 26 | } 27 | 28 | export default PanelItems 29 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelListItem.tsx: -------------------------------------------------------------------------------- 1 | import { useStyletron } from 'baseui' 2 | import Icons from '@components/icons' 3 | import useAppContext from '@/hooks/useAppContext' 4 | import { useEditor } from '@scenify/sdk' 5 | 6 | function PanelListItem({ label, icon, activePanel }: any) { 7 | const { setActivePanel } = useAppContext() 8 | const editor = useEditor() 9 | const [css, theme] = useStyletron() 10 | const Icon = Icons[icon] 11 | return ( 12 |
{ 14 | editor.deselect() 15 | setActivePanel(label) 16 | }} 17 | className={css({ 18 | width: '84px', 19 | height: '80px', 20 | backgroundColor: label === activePanel ? theme.colors.background : theme.colors.primary100, 21 | display: 'flex', 22 | alignItems: 'center', 23 | flexDirection: 'column', 24 | justifyContent: 'center', 25 | fontFamily: 'Uber Move Text', 26 | fontWeight: 500, 27 | fontSize: '0.8rem', 28 | userSelect: 'none', 29 | transition: 'all 0.5s', 30 | gap: '0.1rem', 31 | ':hover': { 32 | cursor: 'pointer', 33 | backgroundColor: theme.colors.background, 34 | transition: 'all 1s', 35 | }, 36 | })} 37 | > 38 | 39 |
{label}
40 |
41 | ) 42 | } 43 | 44 | export default PanelListItem 45 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/Panels.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | import PanelItem from './PanelItem' 3 | import PanelsList from './PanelsList' 4 | 5 | function Panels() { 6 | return ( 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default Panels 15 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/PanelsList.tsx: -------------------------------------------------------------------------------- 1 | import { panelListItems } from '@/constants/app-options' 2 | import useAppContext from '@/hooks/useAppContext' 3 | import { styled } from 'baseui' 4 | import PanelListItem from './PanelListItem' 5 | 6 | const Container = styled('div', props => ({ 7 | width: '84px', 8 | backgroundColor: props.$theme.colors.primary100, 9 | })) 10 | 11 | function PanelsList() { 12 | const { activePanel } = useAppContext() 13 | return ( 14 | 15 | {panelListItems.map(panelListItem => ( 16 | 23 | ))} 24 | 25 | ) 26 | } 27 | 28 | export default PanelsList 29 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Panels/index.tsx: -------------------------------------------------------------------------------- 1 | import Panels from './Panels' 2 | export default Panels 3 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/Toolbox.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor, useEditorContext } from '@scenify/sdk' 2 | import { styled } from 'baseui' 3 | import { useEffect, useState } from 'react' 4 | import ToolboxItems from './ToolboxItems' 5 | import Locked from './ToolboxItems/Locked' 6 | import isArray from 'lodash/isArray' 7 | 8 | const Container = styled('div', props => ({ 9 | height: '56px', 10 | backgroundColor: props.$theme.colors.background, 11 | boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.15)', 12 | marginLeft: '1px', 13 | display: 'flex', 14 | })) 15 | 16 | export const getContextMenuType = (selection: any) => { 17 | const types = new Set() 18 | if (!selection) { 19 | return 'Default' 20 | } 21 | if (selection._objects) { 22 | for (const object of selection._objects) { 23 | types.add(object.type) 24 | } 25 | } else { 26 | types.add(selection.type) 27 | } 28 | 29 | const typesArray = Array.from(types) 30 | 31 | if (typesArray.length === 1) { 32 | if (typesArray[0] === 'Background') { 33 | return 'Default' 34 | } else { 35 | return typesArray[0] 36 | } 37 | } else { 38 | return typesArray 39 | } 40 | } 41 | 42 | const toolboxOptions = { 43 | Default: 'Default', 44 | StaticText: 'StaticText', 45 | StaticPath: 'StaticPath', 46 | StaticImage: 'StaticImage', 47 | MultiElement: 'MultiElement', 48 | DynamicText: 'DynamicText', 49 | DynamicImage: 'DynamicImage', 50 | } 51 | 52 | function EditorToolbox() { 53 | const [activeToolbox, setActiveToolbox] = useState('Default') 54 | const [locked, setLocked] = useState(false) 55 | const { activeObject } = useEditorContext() 56 | const editor = useEditor() 57 | 58 | useEffect(() => { 59 | if (activeObject) { 60 | // @ts-ignore 61 | setLocked(activeObject.locked) 62 | const activeObjectType = getContextMenuType(activeObject) 63 | if (isArray(activeObjectType)) { 64 | // @ts-ignore 65 | setActiveToolbox(toolboxOptions['MultiElement']) 66 | } else { 67 | // @ts-ignore 68 | setActiveToolbox(toolboxOptions[activeObjectType]) 69 | } 70 | } else { 71 | setLocked(false) 72 | setActiveToolbox(null) 73 | } 74 | }, [activeObject]) 75 | 76 | useEffect(() => { 77 | const handleHistoryChange = () => { 78 | if (activeObject) { 79 | // @ts-ignore 80 | setLocked(activeObject.locked) 81 | } else { 82 | setLocked(false) 83 | // setActiveToolbox(null) 84 | } 85 | } 86 | if (editor) { 87 | editor.on('history:changed', handleHistoryChange) 88 | } 89 | return () => { 90 | if (editor) { 91 | editor.off('history:changed', handleHistoryChange) 92 | } 93 | } 94 | }, [editor, activeObject]) 95 | 96 | if (!activeObject) { 97 | return ( 98 | 99 | 100 | 101 | ) 102 | } 103 | if (locked) { 104 | return ( 105 | 106 | 107 | 108 | ) 109 | } 110 | 111 | const Toolbox = activeObject ? ToolboxItems[activeToolbox] : null 112 | return {Toolbox ? : } 113 | } 114 | 115 | export default EditorToolbox 116 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/Default.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../Icons' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import useAppContext from '@/hooks/useAppContext' 4 | import { SubMenuType } from '@/constants/editor' 5 | 6 | function Default() { 7 | const { setActiveSubMenu } = useAppContext() 8 | return ( 9 |
18 | 26 |
27 | ) 28 | } 29 | 30 | export default Default 31 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/Illustration.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../Icons' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import useAppContext from '@/hooks/useAppContext' 4 | import { SubMenuType } from '@/constants/editor' 5 | import Common from './components/Common' 6 | import Animate from './components/Animate' 7 | function Illustration() { 8 | const { setActiveSubMenu } = useAppContext() 9 | 10 | return ( 11 |
20 |
21 | 29 | 30 |
31 | 32 |
33 | ) 34 | } 35 | 36 | export default Illustration 37 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/Image.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../Icons' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import useAppContext from '@/hooks/useAppContext' 4 | import { SubMenuType } from '@/constants/editor' 5 | import Common from './components/Common' 6 | import Animate from './components/Animate' 7 | 8 | function Image() { 9 | const { setActiveSubMenu } = useAppContext() 10 | 11 | return ( 12 |
21 |
22 | 30 | 31 |
32 | 33 |
34 | ) 35 | } 36 | 37 | export default Image 38 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/Locked.tsx: -------------------------------------------------------------------------------- 1 | import Lock from './components/Lock' 2 | function Locked() { 3 | return ( 4 |
13 | 14 |
15 | ) 16 | } 17 | 18 | export default Locked 19 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/MultiElement.tsx: -------------------------------------------------------------------------------- 1 | import Common from './components/Common' 2 | 3 | function MultiElement() { 4 | return ( 5 |
14 |
Multi
15 | 16 |
17 | ) 18 | } 19 | 20 | export default MultiElement 21 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/Path.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../Icons' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import useAppContext from '@/hooks/useAppContext' 4 | import { SubMenuType } from '@/constants/editor' 5 | import Common from './components/Common' 6 | import Animate from './components/Animate' 7 | 8 | function Path() { 9 | const { setActiveSubMenu } = useAppContext() 10 | return ( 11 |
20 |
21 | 29 | 30 |
31 | 32 |
33 | ) 34 | } 35 | 36 | export default Path 37 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/Text.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../Icons' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import { ChevronDown } from 'baseui/icon' 4 | import useAppContext from '@/hooks/useAppContext' 5 | import { SubMenuType } from '@/constants/editor' 6 | import { useEffect, useState } from 'react' 7 | import { useActiveObject, useEditor } from '@scenify/sdk' 8 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 9 | import { StatefulMenu } from 'baseui/menu' 10 | import Spacing from './components/Spacing' 11 | import Common from './components/Common' 12 | import Animate from './components/Animate' 13 | 14 | interface TextOptions { 15 | fontFamily: string 16 | fontSize: number 17 | fontWeight: string | number 18 | opacity: number[] 19 | italic: string 20 | textAligh: string 21 | underline: string 22 | fill: string 23 | } 24 | 25 | const defaultOptions = { 26 | fontFamily: 'Open Sans', 27 | fontWeight: 'normal', 28 | fontSize: 12, 29 | opacity: [1], 30 | italic: 'true', 31 | textAligh: 'center', 32 | underline: 'true', 33 | fill: '#000000', 34 | } 35 | 36 | const ITEMS = [ 37 | { label: 8 }, 38 | { label: 10 }, 39 | { label: 12 }, 40 | { label: 14 }, 41 | { label: 16 }, 42 | { label: 18 }, 43 | { label: 20 }, 44 | { label: 22 }, 45 | { label: 24 }, 46 | { label: 32 }, 47 | { label: 36 }, 48 | { label: 64 }, 49 | ] 50 | 51 | function Text() { 52 | const { setActiveSubMenu } = useAppContext() 53 | const activeObject = useActiveObject() 54 | const [options, setOptions] = useState(defaultOptions) 55 | const editor = useEditor() 56 | useEffect(() => { 57 | updateOptions(activeObject) 58 | }, [activeObject]) 59 | 60 | useEffect(() => { 61 | const handleChanges = () => { 62 | updateOptions(activeObject) 63 | } 64 | editor.on('history:changed', handleChanges) 65 | return () => { 66 | editor.off('history:changed', handleChanges) 67 | } 68 | // eslint-disable-next-line react-hooks/exhaustive-deps 69 | }, [editor]) 70 | 71 | const updateOptions = (object: fabric.TextOptions) => { 72 | const textOptions = ({ 73 | fontFamily: object.fontFamily, 74 | fontSize: object.fontSize, 75 | fontWeight: object.fontWeight, 76 | opacity: [object.opacity * 100], 77 | italic: object.fontStyle, 78 | textAligh: object.textAlign, 79 | underline: object.underline, 80 | fill: object.fill, 81 | } as unknown) as TextOptions 82 | setOptions(textOptions) 83 | } 84 | 85 | const checkBold = (value: string | number) => { 86 | return value === 'bold' || value === 700 87 | } 88 | 89 | const toggleBold = () => { 90 | const isBold = checkBold(options.fontWeight) 91 | editor.update({ fontWeight: isBold ? 400 : 700 }) 92 | } 93 | 94 | const toggleUnderline = () => { 95 | editor.update({ underline: activeObject.underline ? false : true }) 96 | } 97 | 98 | const checkIsItalic = (value: string) => { 99 | const isItalic = value === 'italic' 100 | return isItalic 101 | } 102 | 103 | const toggleItalic = () => { 104 | const isItalic = checkIsItalic(activeObject.fontStyle) 105 | editor.update({ fontStyle: isItalic ? 'normal' : 'italic' }) 106 | } 107 | 108 | const getNextTextAlign = (current: string) => { 109 | const positions = ['left', 'center', 'right', 'left'] 110 | const currentIndex = positions.findIndex(v => v === current) 111 | const nextAlign = positions[currentIndex + 1] 112 | return nextAlign 113 | } 114 | 115 | const toggleTextAlign = () => { 116 | const currentValue = activeObject.textAlign 117 | const nextTextAlign = getNextTextAlign(currentValue) 118 | editor.update({ textAlign: nextTextAlign }) 119 | } 120 | 121 | const getTextAlignIcon = () => { 122 | const currentValue = activeObject.textAlign 123 | const Icon = 124 | currentValue === 'left' 125 | ? Icons.TextAlignLeft 126 | : currentValue === 'center' 127 | ? Icons.TextAlignCenter 128 | : currentValue === 'right' 129 | ? Icons.TextAlignRight 130 | : Icons.TextAlignJustify 131 | return Icon 132 | } 133 | 134 | const updateFontSize = (value: number) => { 135 | editor.update({ fontSize: value }) 136 | } 137 | 138 | const TextAlignIcon = getTextAlignIcon() 139 | return ( 140 |
149 |
150 | 177 | 178 | ( 182 | { 185 | updateFontSize(event.item.label) 186 | close() 187 | }} 188 | overrides={{ 189 | List: { style: { width: '72px', textAlign: 'center', height: '240px' } }, 190 | }} 191 | /> 192 | )} 193 | > 194 | 220 | 221 | 222 |
223 | 231 | 242 | 253 | 264 |
267 | 270 |
273 | 274 | 275 |
276 |
277 | 278 |
279 | ) 280 | } 281 | 282 | export default Text 283 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Animate.tsx: -------------------------------------------------------------------------------- 1 | import { Button, KIND, SIZE } from 'baseui/button' 2 | import useAppContext from '@/hooks/useAppContext' 3 | import Icons from '../../../Icons' 4 | import { SubMenuType } from '@/constants/editor' 5 | 6 | function Animate() { 7 | const { setActiveSubMenu } = useAppContext() 8 | 9 | return ( 10 | 25 | ) 26 | } 27 | 28 | export default Animate 29 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Common.tsx: -------------------------------------------------------------------------------- 1 | import Delete from './Delete' 2 | import Duplicate from './Duplicate' 3 | import Opacity from './Opacity' 4 | import Position from './Position' 5 | import Lock from './Lock' 6 | import CopyStyle from './CopyStyle' 7 | import Group from './Group' 8 | 9 | function Common() { 10 | return ( 11 |
12 | 13 | 14 |
17 | 18 | 19 |
22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | 29 | export default Common 30 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/CopyStyle.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor } from '@scenify/sdk' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import Icons from '../../../Icons' 4 | 5 | function CopyStyle() { 6 | const editor = useEditor() 7 | return ( 8 | 11 | ) 12 | } 13 | 14 | export default CopyStyle 15 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Delete.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor } from '@scenify/sdk' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import Icons from '../../../Icons' 4 | 5 | function Delete() { 6 | const editor = useEditor() 7 | return ( 8 | 11 | ) 12 | } 13 | 14 | export default Delete 15 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Duplicate.tsx: -------------------------------------------------------------------------------- 1 | import { useEditor } from '@scenify/sdk' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import Icons from '../../../Icons' 4 | 5 | function Duplicate() { 6 | const editor = useEditor() 7 | return ( 8 | 18 | ) 19 | } 20 | 21 | export default Duplicate 22 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Group.tsx: -------------------------------------------------------------------------------- 1 | import { useActiveObject, useEditor } from '@scenify/sdk' 2 | import { Button, KIND, SIZE } from 'baseui/button' 3 | import { useEffect, useState } from 'react' 4 | 5 | interface Options { 6 | isGroup: boolean 7 | multiple: boolean 8 | } 9 | 10 | function Locked() { 11 | const [options, setOptions] = useState({ isGroup: false, multiple: false }) 12 | 13 | const editor = useEditor() 14 | const activeObject = useActiveObject() 15 | 16 | useEffect(() => { 17 | if (activeObject) { 18 | updateOptions(activeObject) 19 | } 20 | // eslint-disable-next-line react-hooks/exhaustive-deps 21 | }, [activeObject]) 22 | 23 | const updateOptions = (object: any) => { 24 | const { type } = object 25 | // @ts-ignore 26 | setOptions({ ...options, isGroup: type === 'group', multiple: !!activeObject._objects }) 27 | } 28 | 29 | return ( 30 | <> 31 | {options.multiple && ( 32 | <> 33 | {options.isGroup ? ( 34 | 44 | ) : ( 45 | 55 | )} 56 | 57 | )} 58 | 59 | ) 60 | } 61 | 62 | export default Locked 63 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Lock.tsx: -------------------------------------------------------------------------------- 1 | import { useActiveObject, useEditor } from '@scenify/sdk' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import { useEffect, useState } from 'react' 4 | import Icons from '../../../Icons' 5 | 6 | interface Options { 7 | locked: boolean 8 | } 9 | 10 | function Locked() { 11 | const [options, setOptions] = useState({ locked: false }) 12 | 13 | const editor = useEditor() 14 | const activeObject = useActiveObject() 15 | 16 | useEffect(() => { 17 | if (activeObject) { 18 | updateOptions(activeObject) 19 | } 20 | // eslint-disable-next-line react-hooks/exhaustive-deps 21 | }, [activeObject]) 22 | 23 | const updateOptions = (object: any) => { 24 | const { locked } = object 25 | setOptions({ ...options, locked: !!locked }) 26 | } 27 | 28 | return ( 29 | <> 30 | {options.locked ? ( 31 | 42 | ) : ( 43 | 54 | )} 55 | 56 | ) 57 | } 58 | 59 | export default Locked 60 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Opacity.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../../Icons' 2 | import { Input } from 'baseui/input' 3 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 4 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 5 | import { Slider } from 'baseui/slider' 6 | import { useActiveObject, useEditor } from '@scenify/sdk' 7 | import { useEffect, useState } from 'react' 8 | 9 | function Opacity() { 10 | const [value, setValue] = useState([1]) 11 | const activeObject = useActiveObject() 12 | const editor = useEditor() 13 | useEffect(() => { 14 | updateOptions(activeObject) 15 | }, [activeObject]) 16 | 17 | useEffect(() => { 18 | const handleChanges = () => { 19 | updateOptions(activeObject) 20 | } 21 | editor.on('history:changed', handleChanges) 22 | return () => { 23 | editor.off('history:changed', handleChanges) 24 | } 25 | // eslint-disable-next-line react-hooks/exhaustive-deps 26 | }, [editor]) 27 | 28 | const updateOptions = (object: fabric.IObjectOptions) => { 29 | const updatedValue = [object.opacity * 100] 30 | setValue(updatedValue) 31 | } 32 | 33 | const updateOpacity = (value: number[]) => { 34 | const opacityValue = value[0] / 100 35 | editor.update({ opacity: opacityValue }) 36 | } 37 | 38 | return ( 39 | ( 43 |
52 |
60 |
Transparency
61 | null, 64 | ThumbValue: () => null, 65 | TickBar: () => null, 66 | Thumb: { 67 | style: { 68 | height: '12px', 69 | width: '12px', 70 | }, 71 | }, 72 | }} 73 | min={0} 74 | max={100} 75 | marks={false} 76 | value={value} 77 | onChange={({ value }) => updateOpacity(value)} 78 | /> 79 |
80 | {}} 104 | value={Math.round(value[0])} 105 | /> 106 |
107 |
108 |
109 | )} 110 | > 111 | 114 |
115 | ) 116 | } 117 | 118 | export default Opacity 119 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Position.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../../Icons' 2 | import { Button, KIND, SIZE } from 'baseui/button' 3 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 4 | import { useStyletron } from 'baseui' 5 | import { useEditor } from '@scenify/sdk' 6 | 7 | function Position() { 8 | const editor = useEditor() 9 | return ( 10 | ( 14 |
15 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 |
36 | )} 37 | > 38 | 41 |
42 | ) 43 | } 44 | interface PositionItemProps { 45 | icon: string 46 | label: string 47 | onClick: Function 48 | } 49 | const PositionItem = ({ icon, label, onClick }: PositionItemProps) => { 50 | const Icon = Icons[icon] 51 | const [css] = useStyletron() 52 | return ( 53 |
onClick()} 55 | className={css({ 56 | display: 'flex', 57 | alignItems: 'center', 58 | padding: '0.5rem', 59 | borderRadius: '4px', 60 | ':hover': { 61 | backgroundColor: 'rgba(0,0,0,0.05)', 62 | cursor: 'pointer', 63 | }, 64 | })} 65 | > 66 |
67 | 68 |
{label}
69 |
70 |
71 | ) 72 | } 73 | 74 | export default Position 75 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/components/Spacing.tsx: -------------------------------------------------------------------------------- 1 | import Icons from '../../../Icons' 2 | import { Button, SHAPE, KIND, SIZE } from 'baseui/button' 3 | import { StatefulPopover, PLACEMENT } from 'baseui/popover' 4 | import { Input } from 'baseui/input' 5 | import { Slider } from 'baseui/slider' 6 | import { useActiveObject, useEditor } from '@scenify/sdk' 7 | import { useEffect, useState } from 'react' 8 | 9 | interface Options { 10 | charSpacing: number 11 | lineHeight: number 12 | } 13 | 14 | function Spacing() { 15 | const activeObject = useActiveObject() 16 | const [options, setOptions] = useState({ charSpacing: 0, lineHeight: 0 }) 17 | 18 | const editor = useEditor() 19 | useEffect(() => { 20 | updateOptions(activeObject) 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | }, [activeObject]) 23 | 24 | useEffect(() => { 25 | if (activeObject) { 26 | updateOptions(activeObject) 27 | } 28 | // eslint-disable-next-line react-hooks/exhaustive-deps 29 | }, [activeObject]) 30 | 31 | const updateOptions = (object: any) => { 32 | const { charSpacing, lineHeight } = object 33 | setOptions({ ...options, charSpacing: charSpacing / 10, lineHeight: lineHeight * 10 }) 34 | } 35 | 36 | const handleChange = (type: string, value: number[]) => { 37 | if (editor) { 38 | if (type === 'charSpacing') { 39 | setOptions({ ...options, [type]: value[0] }) 40 | 41 | editor.update({ 42 | [type]: value[0] * 10, 43 | }) 44 | } else { 45 | setOptions({ ...options, [type]: value[0] }) 46 | 47 | editor.update({ 48 | [type]: value[0] / 10, 49 | }) 50 | } 51 | } 52 | } 53 | 54 | return ( 55 | ( 59 |
71 |
79 |
Letter spacing
80 |
81 | null, 84 | ThumbValue: () => null, 85 | TickBar: () => null, 86 | Thumb: { 87 | style: { 88 | height: '12px', 89 | width: '12px', 90 | }, 91 | }, 92 | }} 93 | min={-20} 94 | max={100} 95 | marks={false} 96 | value={[options.charSpacing]} 97 | onChange={({ value }) => handleChange('charSpacing', value)} 98 | /> 99 |
100 |
101 | {}} 125 | value={Math.round(options.charSpacing)} 126 | /> 127 |
128 |
129 |
137 |
Line height
138 |
139 | null, 142 | ThumbValue: () => null, 143 | TickBar: () => null, 144 | Thumb: { 145 | style: { 146 | height: '12px', 147 | width: '12px', 148 | }, 149 | }, 150 | }} 151 | min={0} 152 | max={100} 153 | marks={false} 154 | value={[options.lineHeight]} 155 | onChange={({ value }) => handleChange('lineHeight', value)} 156 | /> 157 |
158 |
159 | {}} 183 | value={Math.round(options.lineHeight)} 184 | /> 185 |
186 |
187 |
188 | )} 189 | > 190 | 193 |
194 | ) 195 | } 196 | 197 | export default Spacing 198 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/ToolboxItems/index.ts: -------------------------------------------------------------------------------- 1 | import Default from './Default' 2 | import Text from './Text' 3 | import Path from './Path' 4 | import Illustration from './Illustration' 5 | import Image from './Image' 6 | import MultiElement from './MultiElement' 7 | class ToolboxItems { 8 | static Default = Default 9 | static StaticText = Text 10 | static DynamicText = Text 11 | static StaticPath = Path 12 | static StaticVector = Illustration 13 | static StaticImage = Image 14 | static DynamicImage = Image 15 | static MultiElement = MultiElement 16 | } 17 | 18 | export default ToolboxItems 19 | -------------------------------------------------------------------------------- /src/scenes/Editor/components/Toolbox/index.tsx: -------------------------------------------------------------------------------- 1 | import Toolbox from './Toolbox' 2 | export default Toolbox 3 | -------------------------------------------------------------------------------- /src/scenes/Editor/index.ts: -------------------------------------------------------------------------------- 1 | import Editor from './Editor' 2 | export default Editor 3 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import { IElement, IFontFamily, IUpload } from '@/interfaces/editor' 2 | import axios, { AxiosInstance } from 'axios' 3 | 4 | type Template = any 5 | class ApiService { 6 | base: AxiosInstance 7 | constructor() { 8 | this.base = axios.create({ 9 | baseURL: 'https://api.scenify.io', 10 | headers: { 11 | Authorization: 'Bearer 9xfZNknNmE4ALeYS6ZCWX8pb', 12 | }, 13 | }) 14 | } 15 | 16 | // UPLOADS 17 | getSignedURLForUpload(props: { name: string }): Promise<{ url: string }> { 18 | return new Promise((resolve, reject) => { 19 | this.base 20 | .post('/uploads', props) 21 | .then(({ data }) => { 22 | resolve(data) 23 | }) 24 | .catch(err => reject(err)) 25 | }) 26 | } 27 | 28 | updateUploadFile(props: { name: string }): Promise { 29 | return new Promise((resolve, reject) => { 30 | this.base 31 | .put('/uploads', props) 32 | .then(({ data }) => { 33 | resolve(data) 34 | }) 35 | .catch(err => reject(err)) 36 | }) 37 | } 38 | 39 | getUploads(): Promise { 40 | return new Promise(async (resolve, reject) => { 41 | try { 42 | const { data } = await this.base.get('/uploads') 43 | resolve(data.data) 44 | } catch (err) { 45 | reject(err) 46 | } 47 | }) 48 | } 49 | 50 | deleteUpload(id: string) { 51 | return new Promise(async (resolve, reject) => { 52 | try { 53 | const response = await this.base.delete(`/uploads/${id}`) 54 | resolve(response) 55 | } catch (err) { 56 | reject(err) 57 | } 58 | }) 59 | } 60 | 61 | // TEMPLATES 62 | 63 | createTemplate(props: Partial