├── .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 | 
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 |
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 |
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 |
9 | )
10 | }
11 |
12 | export default Background
13 |
--------------------------------------------------------------------------------
/src/components/icons/Elements.tsx:
--------------------------------------------------------------------------------
1 | function Elements({ size }: { size: number }) {
2 | return (
3 |
12 | )
13 | }
14 |
15 | export default Elements
16 |
--------------------------------------------------------------------------------
/src/components/icons/Illustrations.tsx:
--------------------------------------------------------------------------------
1 | function Illustrations({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Illustrations
13 |
--------------------------------------------------------------------------------
/src/components/icons/Images.tsx:
--------------------------------------------------------------------------------
1 | function Images({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Images
13 |
--------------------------------------------------------------------------------
/src/components/icons/Logo.tsx:
--------------------------------------------------------------------------------
1 | function Logo({ size }: { size: number }) {
2 | return (
3 |
29 | )
30 | }
31 |
32 | export default Logo
33 |
--------------------------------------------------------------------------------
/src/components/icons/Pixabay.tsx:
--------------------------------------------------------------------------------
1 | function Pixabay({ size }: { size: number }) {
2 | return (
3 |
13 | )
14 | }
15 |
16 | export default Pixabay
17 |
--------------------------------------------------------------------------------
/src/components/icons/Search.tsx:
--------------------------------------------------------------------------------
1 | function Search({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Search
13 |
--------------------------------------------------------------------------------
/src/components/icons/Templates.tsx:
--------------------------------------------------------------------------------
1 | function Templates({ size }: { size: number }) {
2 | return (
3 |
10 | )
11 | }
12 |
13 | export default Templates
14 |
--------------------------------------------------------------------------------
/src/components/icons/Text.tsx:
--------------------------------------------------------------------------------
1 | function Text({ size }: { size: number }) {
2 | return (
3 |
10 | )
11 | }
12 | export default Text
13 |
--------------------------------------------------------------------------------
/src/components/icons/Uploads.tsx:
--------------------------------------------------------------------------------
1 | function Uploads({ size }: { size: number }) {
2 | return (
3 |
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 |

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 |

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 |

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 |
9 | )
10 | }
11 |
12 | export default Background
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Backward.tsx:
--------------------------------------------------------------------------------
1 | function Backward({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Backward
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Bold.tsx:
--------------------------------------------------------------------------------
1 | function Bold({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Bold
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/ChevronLeft.tsx:
--------------------------------------------------------------------------------
1 | function ChevronLeft({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default ChevronLeft
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/CopyStyle.tsx:
--------------------------------------------------------------------------------
1 | function CopyStyle({ size }: { size: number }) {
2 | return (
3 |
11 | )
12 | }
13 |
14 | export default CopyStyle
15 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Delete.tsx:
--------------------------------------------------------------------------------
1 | function Delete({ size }: { size: number }) {
2 | return (
3 |
11 | )
12 | }
13 |
14 | export default Delete
15 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Duplicate.tsx:
--------------------------------------------------------------------------------
1 | function Duplicate({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Duplicate
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Elements.tsx:
--------------------------------------------------------------------------------
1 | function Elements({ size }: { size: number }) {
2 | return (
3 |
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 |

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 |
9 | )
10 | }
11 |
12 | export default Forward
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Images.tsx:
--------------------------------------------------------------------------------
1 | function Images({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Images
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Italic.tsx:
--------------------------------------------------------------------------------
1 | function Italic({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Italic
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Locked.tsx:
--------------------------------------------------------------------------------
1 | function Locked({ size }: { size: number }) {
2 | return (
3 |
10 | )
11 | }
12 |
13 | export default Locked
14 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Logo.tsx:
--------------------------------------------------------------------------------
1 | function Logo({ size }: { size: number }) {
2 | return (
3 |
29 | )
30 | }
31 |
32 | export default Logo
33 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Opacity..tsx:
--------------------------------------------------------------------------------
1 | function Opacity({ size }: { size: number }) {
2 | return (
3 |
29 | )
30 | }
31 |
32 | export default Opacity
33 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Redo.tsx:
--------------------------------------------------------------------------------
1 | function Redo({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Redo
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Spacing.tsx:
--------------------------------------------------------------------------------
1 | function Spacing({ size }: { size: number }) {
2 | return (
3 |
21 | )
22 | }
23 |
24 | export default Spacing
25 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Templates.tsx:
--------------------------------------------------------------------------------
1 | function Templates({ size }: { size: number }) {
2 | return (
3 |
10 | )
11 | }
12 |
13 | export default Templates
14 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Text.tsx:
--------------------------------------------------------------------------------
1 | function Text({ size }: { size: number }) {
2 | return (
3 |
10 | )
11 | }
12 | export default Text
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/TextAlignCenter.tsx:
--------------------------------------------------------------------------------
1 | function TextAlignCenter({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 | export default TextAlignCenter
12 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/TextAlignJustify.tsx:
--------------------------------------------------------------------------------
1 | function TextAlignJustify({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default TextAlignJustify
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/TextAlignLeft.tsx:
--------------------------------------------------------------------------------
1 | function TextAlignLeft({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default TextAlignLeft
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/TextAlignRight.tsx:
--------------------------------------------------------------------------------
1 | function TextAlignRight({ size }: { size: number }) {
2 | return (
3 |
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 |
10 | {color === '#000000' || color === '#ffffff' ? (
11 |
12 |

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 |
12 | )
13 | }
14 |
15 | export default TextSpacing
16 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/TimeFast.tsx:
--------------------------------------------------------------------------------
1 | function Timefast({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Timefast
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/ToBack.tsx:
--------------------------------------------------------------------------------
1 | function ToBack({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default ToBack
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/ToFront.tsx:
--------------------------------------------------------------------------------
1 | function ToFront({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 | export default ToFront
12 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Underline.tsx:
--------------------------------------------------------------------------------
1 | function Underline({ size }: { size: number }) {
2 | return (
3 |
10 | )
11 | }
12 |
13 | export default Underline
14 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Undo.tsx:
--------------------------------------------------------------------------------
1 | function Undo({ size }: { size: number }) {
2 | return (
3 |
9 | )
10 | }
11 |
12 | export default Undo
13 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Unlocked.tsx:
--------------------------------------------------------------------------------
1 | function Elements({ size }: { size: number }) {
2 | return (
3 |
10 | )
11 | }
12 |
13 | export default Elements
14 |
--------------------------------------------------------------------------------
/src/scenes/Editor/components/Icons/Uploads.tsx:
--------------------------------------------------------------------------------
1 | function Uploads({ size }: { size: number }) {
2 | return (
3 |
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 | }
82 | kind={KIND.tertiary}
83 | onClick={() => history.push('/')}
84 | >
85 | Home
86 |
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 |

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 |
69 |
70 |
Custom size
71 |
72 | 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 |

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 |
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 |
118 |
119 |
120 |
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 |
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 |

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 |

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 |

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 |

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 &&

}
111 |
112 | {uploads.map(upload => (
113 |
addImageToCanvas(upload.url)}
121 | >
122 |
123 |

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 |
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 |
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 |
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): Promise {
64 | return new Promise((resolve, reject) => {
65 | this.base
66 | .post('/templates', props)
67 | .then(({ data }) => {
68 | resolve(data)
69 | })
70 | .catch(err => reject(err))
71 | })
72 | }
73 |
74 | downloadTemplate(props: Partial): Promise<{ source: string }> {
75 | return new Promise((resolve, reject) => {
76 | this.base
77 | .post('/templates/download', props)
78 | .then(({ data }) => {
79 | resolve(data)
80 | })
81 | .catch(err => reject(err))
82 | })
83 | }
84 |
85 | getTemplates(): Promise {
86 | return new Promise(async (resolve, reject) => {
87 | try {
88 | const { data } = await this.base.get('/templates')
89 | resolve(data)
90 | } catch (err) {
91 | reject(err)
92 | }
93 | })
94 | }
95 |
96 | getTemplateById(id: string): Promise {
97 | return new Promise(async (resolve, reject) => {
98 | try {
99 | const { data } = await this.base.get(`/templates/${id}`)
100 | resolve(data)
101 | } catch (err) {
102 | reject(err)
103 | }
104 | })
105 | }
106 | //CREATIONS
107 |
108 | createCreation(props: Partial): Promise {
109 | return new Promise((resolve, reject) => {
110 | this.base
111 | .post('/creations', props)
112 | .then(({ data }) => {
113 | resolve(data)
114 | })
115 | .catch(err => reject(err))
116 | })
117 | }
118 |
119 | getCreations(): Promise {
120 | return new Promise(async (resolve, reject) => {
121 | try {
122 | const { data } = await this.base.get('/creations')
123 | resolve(data)
124 | } catch (err) {
125 | reject(err)
126 | }
127 | })
128 | }
129 |
130 | getCreationById(id: string): Promise {
131 | return new Promise(async (resolve, reject) => {
132 | try {
133 | const { data } = await this.base.get(`/creations/${id}`)
134 | resolve(data)
135 | } catch (err) {
136 | reject(err)
137 | }
138 | })
139 | }
140 | updateCreation(id: string, props: Partial): Promise {
141 | return new Promise((resolve, reject) => {
142 | this.base
143 | .put(`/creations/${id}`, props)
144 | .then(({ data }) => {
145 | resolve(data)
146 | })
147 | .catch(err => reject(err))
148 | })
149 | }
150 |
151 | // ELEMENTS
152 | getElements(): Promise {
153 | return new Promise(async (resolve, reject) => {
154 | try {
155 | const { data } = await this.base.get('/elements')
156 | resolve(data)
157 | } catch (err) {
158 | reject(err)
159 | }
160 | })
161 | }
162 | updateTemplate(id: number, props: Partial): Promise {
163 | return new Promise((resolve, reject) => {
164 | this.base
165 | .put(`/templates/${id}`, props)
166 | .then(({ data }) => {
167 | resolve(data)
168 | })
169 | .catch(err => reject(err))
170 | })
171 | }
172 | // FONTS
173 | getFonts(): Promise {
174 | return new Promise(async (resolve, reject) => {
175 | try {
176 | const { data } = await this.base.get('/fonts')
177 | resolve(data)
178 | } catch (err) {
179 | reject(err)
180 | }
181 | })
182 | }
183 | }
184 |
185 | export default new ApiService()
186 |
--------------------------------------------------------------------------------
/src/services/iconscout.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const iconscoutClient = axios.create({
4 | baseURL: 'https://api.iconscout.com/v3/',
5 | headers: {
6 | 'Client-ID': process.env.REACT_APP_ICONSCOUT_CLIENT_ID,
7 | 'Client-Secret': process.env.REACT_APP_ICONSCOUT_SECRET,
8 | },
9 | })
10 |
11 | export function getImages(query) {
12 | return new Promise((resolve, reject) => {
13 | iconscoutClient
14 | .get('search', {
15 | params: {
16 | query: query,
17 | product_type: 'item',
18 | asset: 'illustration',
19 | price: 'free',
20 | per_page: 20,
21 | page: 1,
22 | formats: ['svg'],
23 | styles: ['colored-outline'],
24 | sort: 'color',
25 | },
26 | })
27 | .then(response => {
28 | const items = response.data.response.items.data
29 | resolve(items)
30 | })
31 | .catch(err => {
32 | reject(err)
33 | })
34 | })
35 | }
36 |
37 | export function getImage(uuid): Promise {
38 | return new Promise((resolve, reject) => {
39 | iconscoutClient
40 | .post(`items/${uuid}/api-download`, {
41 | format: 'svg',
42 | })
43 | .then(response => {
44 | const imageURL = response.data.response.download.url
45 | resolve(imageURL)
46 | })
47 | .catch(err => {
48 | reject(err)
49 | })
50 | })
51 | }
52 |
--------------------------------------------------------------------------------
/src/services/pixabay.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const pixabayClient = axios.create({
4 | baseURL: 'https://pixabay.com/api/',
5 | })
6 |
7 | const PIXABAY_KEY = process.env.REACT_APP_PIXABAY_KEY
8 |
9 | export interface PixabayImage {
10 | id: string
11 | webformatURL: string
12 | previewURL: string
13 | }
14 | export function getPixabayImages(query: string): Promise {
15 | let encodedWord = query.replace(/\s+/g, '+').toLowerCase()
16 | return new Promise((resolve, reject) => {
17 | pixabayClient
18 | .get(`?key=${PIXABAY_KEY}&q=${encodedWord}&image_type=photo`)
19 | .then(response => {
20 | resolve(response.data.hits)
21 | })
22 | .catch(err => reject(err))
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom'
6 |
--------------------------------------------------------------------------------
/src/store/rootReducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from '@reduxjs/toolkit'
2 | import persistReducer from 'redux-persist/es/persistReducer'
3 | import storage from 'redux-persist/lib/storage'
4 | import { elementsReducer, ElementsState } from './slices/elements/reducer'
5 | import { uploadsReducer } from './slices/uploads/reducer'
6 | import { PersistConfig } from 'redux-persist/es/types'
7 | import { fontsReducer } from './slices/fonts/reducer'
8 | import { templatesReducer } from './slices/templates/reducer'
9 | import { creationsReducer } from './slices/creations/reducer'
10 | // import { authReducer } from "@store/slices/auth/reducer"
11 | // import { AuthState } from "./slices/auth/reducer"
12 | // import { occasionsReducer } from "./slices/occasions/reducer"
13 | // import { detailsReducer, DetailsState } from "./slices/details/reducer"
14 | // import { feedbackReducer } from "./slices/feedback/reducer"
15 |
16 | // const authPersistConfig: PersistConfig = {
17 | // key: "auth",
18 | // storage,
19 | // blacklist: ["errors", "passwordChageStatus"],
20 | // }
21 |
22 | const elementsPersistConfig: PersistConfig = {
23 | key: 'elements',
24 | storage,
25 | }
26 |
27 | const rootReducer = combineReducers({
28 | editor: combineReducers({
29 | elements: persistReducer(elementsPersistConfig, elementsReducer),
30 | uploads: uploadsReducer,
31 | fonts: fontsReducer,
32 | templates: templatesReducer,
33 | }),
34 | creations: creationsReducer,
35 | // auth: persistReducer(authPersistConfig, authReducer),
36 | // occassions: occasionsReducer,
37 | // details: persistReducer(detailsPersistConfig, detailsReducer),
38 | // feedback: feedbackReducer,
39 | })
40 |
41 | export type RootState = ReturnType
42 |
43 | export default rootReducer
44 |
--------------------------------------------------------------------------------
/src/store/slices/creations/actions.ts:
--------------------------------------------------------------------------------
1 | import { Template } from '@/interfaces/editor'
2 | import { createAsyncThunk, createAction } from '@reduxjs/toolkit'
3 | import api from '@services/api'
4 | import { AxiosError } from 'axios'
5 |
6 | export const setCreations = createAction('creations/setCreations')
7 | export const updateCreationsList = createAction('creations/updateCreationList')
8 |
9 | export const getCreations = createAsyncThunk }>(
10 | 'creations/getCreations',
11 | async (_, { rejectWithValue, dispatch }) => {
12 | try {
13 | const creations = await api.getCreations()
14 | dispatch(setCreations(creations))
15 | } catch (err) {
16 | return rejectWithValue((err as AxiosError).response?.data?.error.data || null)
17 | }
18 | }
19 | )
20 |
21 | export const createCreation = createAsyncThunk(
22 | 'creations/createCreation',
23 | async (args, { dispatch }) => {
24 | const savedCreation = await api.createCreation(args.creation)
25 | dispatch(setCreations([savedCreation]))
26 | }
27 | )
28 |
--------------------------------------------------------------------------------
/src/store/slices/creations/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Template } from '@/interfaces/editor'
2 | import { createReducer, current } from '@reduxjs/toolkit'
3 | import { setCreations, updateCreationsList } from './actions'
4 |
5 | export interface CreationsState {
6 | creations: Template[]
7 | }
8 |
9 | const initialState: CreationsState = {
10 | creations: [],
11 | }
12 |
13 | export const creationsReducer = createReducer(initialState, builder => {
14 | builder.addCase(setCreations, (state, { payload }) => {
15 | state.creations = state.creations.length > 1 ? state.creations.concat(payload) : payload
16 | })
17 | builder.addCase(updateCreationsList, (state, { payload }) => {
18 | const currentState = current(state)
19 | const updatedCreations = currentState.creations.map(creation => {
20 | if (creation.id === payload.id) {
21 | return payload
22 | } else {
23 | return creation
24 | }
25 | })
26 | state.creations = updatedCreations
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/store/slices/creations/selectors.ts:
--------------------------------------------------------------------------------
1 | import { RootState } from '@store/rootReducer'
2 |
3 | export const selectCreations = (state: RootState) => state.creations.creations
4 |
--------------------------------------------------------------------------------
/src/store/slices/elements/actions.ts:
--------------------------------------------------------------------------------
1 | import { IElement } from '@/interfaces/editor'
2 | import { createAsyncThunk, createAction } from '@reduxjs/toolkit'
3 | import api from '@services/api'
4 | import { AxiosError } from 'axios'
5 |
6 | export const setElements = createAction('elements/setlements')
7 |
8 | export const getElements = createAsyncThunk }>(
9 | 'elements/getElements',
10 | async (_, { rejectWithValue, dispatch }) => {
11 | try {
12 | const elements = await api.getElements()
13 | dispatch(setElements(elements))
14 | } catch (err) {
15 | return rejectWithValue((err as AxiosError).response?.data?.error.data || null)
16 | }
17 | }
18 | )
19 |
--------------------------------------------------------------------------------
/src/store/slices/elements/reducer.ts:
--------------------------------------------------------------------------------
1 | import { IElement } from '@/interfaces/editor'
2 | import { createReducer } from '@reduxjs/toolkit'
3 | import { setElements } from './actions'
4 |
5 | export interface ElementsState {
6 | elements: IElement[]
7 | }
8 |
9 | const initialState: ElementsState = {
10 | elements: [],
11 | }
12 |
13 | export const elementsReducer = createReducer(initialState, builder => {
14 | builder.addCase(setElements, (state, { payload }) => {
15 | state.elements = payload
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/store/slices/elements/selectors.ts:
--------------------------------------------------------------------------------
1 | import { RootState } from '@store/rootReducer'
2 |
3 | export const selectElements = (state: RootState) => state.editor.elements.elements
4 |
--------------------------------------------------------------------------------
/src/store/slices/fonts/actions.ts:
--------------------------------------------------------------------------------
1 | import { IFontFamily } from '@/interfaces/editor'
2 | import { createAsyncThunk, createAction } from '@reduxjs/toolkit'
3 | import api from '@services/api'
4 | import { AxiosError } from 'axios'
5 |
6 | export const setFonts = createAction('fonts/setFonts')
7 |
8 | export const getFonts = createAsyncThunk }>(
9 | 'fonts/getFonts',
10 | async (_, { rejectWithValue, dispatch }) => {
11 | try {
12 | const fonts = await api.getFonts()
13 | dispatch(setFonts(fonts))
14 | } catch (err) {
15 | return rejectWithValue((err as AxiosError).response?.data?.error.data || null)
16 | }
17 | }
18 | )
19 |
--------------------------------------------------------------------------------
/src/store/slices/fonts/reducer.ts:
--------------------------------------------------------------------------------
1 | import { IFontFamily } from '@/interfaces/editor'
2 | import { createReducer } from '@reduxjs/toolkit'
3 | import { setFonts } from './actions'
4 |
5 | export interface FontsState {
6 | fonts: IFontFamily[]
7 | }
8 |
9 | const initialState: FontsState = {
10 | fonts: [],
11 | }
12 |
13 | export const fontsReducer = createReducer(initialState, builder => {
14 | builder.addCase(setFonts, (state, { payload }) => {
15 | state.fonts = payload
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/store/slices/fonts/selectors.ts:
--------------------------------------------------------------------------------
1 | import { RootState } from '@store/rootReducer'
2 |
3 | export const selectFonts = (state: RootState) => state.editor.fonts.fonts
4 |
--------------------------------------------------------------------------------
/src/store/slices/templates/actions.ts:
--------------------------------------------------------------------------------
1 | import { Template } from '@/interfaces/editor'
2 | import { createAsyncThunk, createAction } from '@reduxjs/toolkit'
3 | import api from '@services/api'
4 | import { AxiosError } from 'axios'
5 |
6 | export const setTemplates = createAction('templates/setTemplates')
7 |
8 | export const getTemplates = createAsyncThunk }>(
9 | 'templates/getTemplates',
10 | async (_, { rejectWithValue, dispatch }) => {
11 | try {
12 | const templates = await api.getTemplates()
13 | dispatch(setTemplates(templates))
14 | } catch (err) {
15 | return rejectWithValue((err as AxiosError).response?.data?.error.data || null)
16 | }
17 | }
18 | )
19 |
--------------------------------------------------------------------------------
/src/store/slices/templates/reducer.ts:
--------------------------------------------------------------------------------
1 | import { Template } from '@/interfaces/editor'
2 | import { createReducer } from '@reduxjs/toolkit'
3 | import { setTemplates } from './actions'
4 |
5 | export interface TemplatesState {
6 | templates: Template[]
7 | }
8 |
9 | const initialState: TemplatesState = {
10 | templates: [],
11 | }
12 |
13 | export const templatesReducer = createReducer(initialState, builder => {
14 | builder.addCase(setTemplates, (state, { payload }) => {
15 | state.templates = payload
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/store/slices/templates/selectors.ts:
--------------------------------------------------------------------------------
1 | import { RootState } from '@store/rootReducer'
2 |
3 | export const selectTemplates = (state: RootState) => state.editor.templates.templates
4 |
--------------------------------------------------------------------------------
/src/store/slices/uploads/actions.ts:
--------------------------------------------------------------------------------
1 | import { IUpload, Uploading } from '@/interfaces/editor'
2 | import { uniqueFilename } from '@/utils/unique'
3 | import { createAsyncThunk, createAction } from '@reduxjs/toolkit'
4 | import api from '@services/api'
5 | import axios, { AxiosError } from 'axios'
6 |
7 | export const setUploads = createAction('uploads/setUploads')
8 | export const setUploading = createAction('uploads/setUploading')
9 | export const closeUploading = createAction('uploads/closeUploading')
10 |
11 | export const getUploads = createAsyncThunk }>(
12 | 'uploads/getUploads',
13 | async (_, { rejectWithValue, dispatch }) => {
14 | try {
15 | const uploads = await api.getUploads()
16 | dispatch(setUploads(uploads))
17 | } catch (err) {
18 | return rejectWithValue((err as AxiosError).response?.data?.error.data || null)
19 | }
20 | }
21 | )
22 |
23 | export const uploadFile = createAsyncThunk(
24 | 'uploads/uploadFile',
25 | async (args, { dispatch }) => {
26 | const file = args.file
27 | setUploading({
28 | progress: 0,
29 | status: 'IN_PROGRESS',
30 | })
31 | const updatedFileName = uniqueFilename(file.name)
32 | const updatedFile = new File([file], updatedFileName)
33 | const response = await api.getSignedURLForUpload({ name: updatedFileName })
34 | await axios.put(response.url, updatedFile, {
35 | headers: { 'Content-Type': 'image/png' },
36 | onUploadProgress: progressEvent => {
37 | const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
38 | setUploading({
39 | progress: percentCompleted,
40 | status: 'IN_PROGRESS',
41 | })
42 | },
43 | })
44 | const uploadedFile = await api.updateUploadFile({ name: updatedFileName })
45 | dispatch(closeUploading())
46 | dispatch(setUploads([uploadedFile]))
47 | }
48 | )
49 |
--------------------------------------------------------------------------------
/src/store/slices/uploads/reducer.ts:
--------------------------------------------------------------------------------
1 | import { IUpload, Uploading } from '@/interfaces/editor'
2 | import { createReducer } from '@reduxjs/toolkit'
3 | import { closeUploading, setUploading, setUploads } from './actions'
4 |
5 | export interface UploadsState {
6 | uploads: IUpload[]
7 | uploading: Uploading | null
8 | }
9 |
10 | const initialState: UploadsState = {
11 | uploads: [],
12 | uploading: null,
13 | }
14 |
15 | export const uploadsReducer = createReducer(initialState, builder => {
16 | builder.addCase(setUploads, (state, { payload }) => {
17 | state.uploads.unshift(...payload)
18 | })
19 | builder.addCase(setUploading, (state, { payload }) => {
20 | state.uploading = payload
21 | })
22 | builder.addCase(closeUploading, state => {
23 | state.uploading = null
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/store/slices/uploads/selectors.ts:
--------------------------------------------------------------------------------
1 | import { RootState } from '@store/rootReducer'
2 |
3 | export const selectUploads = (state: RootState) => state.editor.uploads.uploads
4 | export const selectUploading = (state: RootState) => state.editor.uploads.uploading
5 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore, Action } from "@reduxjs/toolkit"
2 | import { useDispatch } from "react-redux"
3 | import {
4 | persistStore,
5 | FLUSH,
6 | REHYDRATE,
7 | PAUSE,
8 | PERSIST,
9 | PURGE,
10 | REGISTER,
11 | } from "redux-persist"
12 | import { ThunkAction } from "redux-thunk"
13 | import rootReducer, { RootState } from "./rootReducer"
14 |
15 | const store = configureStore({
16 | reducer: rootReducer,
17 | middleware: (getDefaultMiddleware) =>
18 | getDefaultMiddleware({
19 | serializableCheck: {
20 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
21 | },
22 | }),
23 | })
24 |
25 | export const persistor = persistStore(store)
26 |
27 | export type AppDispatch = typeof store.dispatch
28 |
29 | export const useAppDispatch = () => useDispatch()
30 |
31 | export type AppThunk = ThunkAction>
32 |
33 | export default store
34 |
--------------------------------------------------------------------------------
/src/utils/unique.ts:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from "nanoid"
2 |
3 | const nanoid = customAlphabet("_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 10)
4 |
5 | export default function uniqueId() {
6 | return nanoid()
7 | }
8 |
9 | export function uniqueFilename(name: string) {
10 | const nameArray = name.split(".")
11 | const extension = nameArray[nameArray.length - 1]
12 | const uniqueName = [uniqueId(), extension].join(".")
13 | return uniqueName
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfigExtra.json",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": false,
15 | "forceConsistentCasingInFileNames": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "noEmit": true,
22 | "jsx": "react-jsx"
23 | },
24 | "include": [
25 | "src"
26 | ],
27 | "exclude": [
28 | "server"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfigExtra.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["src/*"],
6 | "@assets/*": ["src/assets/*"],
7 | "@components/*": ["src/components/*"],
8 | "@common/*": ["src/common/*"],
9 | "@contexts/*": ["src/contexts/*"],
10 | "@hooks/*": ["src/hooks/*"],
11 | "@handlers/*": ["src/handlers/*"],
12 | "@scenes/*": ["src/scenes/*"],
13 | "@store/*": ["src/store/*"],
14 | "@services/*": ["src/services/*"],
15 | "@utils/*": ["src/utils/*"]
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------