├── .eslintrc.cjs
├── .gitignore
├── CHANGELOG.md
├── README.md
├── components.json
├── images
└── preview.png
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
└── vite.svg
├── src
├── App.css
├── App.tsx
├── assets
│ ├── logo-dark-full.png
│ ├── logo-dark.png
│ └── react.svg
├── components
│ ├── canvas
│ │ ├── actions.ts
│ │ ├── canvas.tsx
│ │ ├── events.ts
│ │ └── fabric
│ │ │ ├── artboard.ts
│ │ │ ├── canvas.ts
│ │ │ └── utils
│ │ │ ├── controls.ts
│ │ │ ├── create-item.ts
│ │ │ ├── drawer.ts
│ │ │ └── get-object-details.ts
│ ├── editor
│ │ ├── control-item
│ │ │ ├── basic-audio.tsx
│ │ │ ├── basic-image.tsx
│ │ │ ├── basic-text.tsx
│ │ │ ├── common
│ │ │ │ ├── opacity.tsx
│ │ │ │ └── transform.tsx
│ │ │ ├── control-item.tsx
│ │ │ ├── index.tsx
│ │ │ ├── presets.tsx
│ │ │ └── smart.tsx
│ │ ├── control-list.tsx
│ │ ├── editor.tsx
│ │ ├── footer.tsx
│ │ ├── index.ts
│ │ ├── menu-item
│ │ │ ├── elements.tsx
│ │ │ ├── images.tsx
│ │ │ ├── index.tsx
│ │ │ ├── menu-item.tsx
│ │ │ ├── texts.tsx
│ │ │ └── uploads.tsx
│ │ ├── menu-list.tsx
│ │ ├── navbar.tsx
│ │ ├── resize-video.tsx
│ │ └── use-hotkeys.ts
│ ├── shared
│ │ └── icons.tsx
│ ├── theme-provider.tsx
│ └── ui
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input-color.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── scroll-area.tsx
│ │ ├── slider.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toggle-group.tsx
│ │ └── toggle.tsx
├── data.ts
├── data
│ ├── audio.ts
│ ├── fonts.ts
│ ├── images.ts
│ ├── shapes.ts
│ ├── uploads.ts
│ └── video.ts
├── global
│ ├── dispatcher.ts
│ ├── events.ts
│ └── index.ts
├── globals.css
├── index.css
├── interfaces
│ ├── editor.ts
│ ├── layout.ts
│ └── rxjs.ts
├── lib
│ └── utils.ts
├── main.tsx
├── store
│ ├── use-data-state.ts
│ └── use-layout-store.ts
├── types.d.ts
├── utils
│ └── fonts.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .vercel
26 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # video-editor
2 |
3 | ## 0.0.13
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies
8 | - @designcombo/core@0.4.8
9 |
10 | ## 0.0.12
11 |
12 | ### Patch Changes
13 |
14 | - Updated dependencies
15 | - @designcombo/core@0.4.7
16 |
17 | ## 0.0.11
18 |
19 | ### Patch Changes
20 |
21 | - Updated dependencies
22 | - @designcombo/core@0.4.6
23 |
24 | ## 0.0.10
25 |
26 | ### Patch Changes
27 |
28 | - Updated dependencies
29 | - @designcombo/core@0.4.5
30 |
31 | ## 0.0.9
32 |
33 | ### Patch Changes
34 |
35 | - Updated dependencies
36 | - @designcombo/core@0.4.4
37 |
38 | ## 0.0.8
39 |
40 | ### Patch Changes
41 |
42 | - Updated dependencies
43 | - @designcombo/core@0.4.3
44 |
45 | ## 0.0.7
46 |
47 | ### Patch Changes
48 |
49 | - Updated dependencies
50 | - @designcombo/core@0.4.2
51 |
52 | ## 0.0.6
53 |
54 | ### Patch Changes
55 |
56 | - Updated dependencies
57 | - @designcombo/core@0.4.1
58 |
59 | ## 0.0.5
60 |
61 | ### Patch Changes
62 |
63 | - Updated dependencies
64 | - @designcombo/core@0.3.1
65 |
66 | ## 0.0.4
67 |
68 | ### Patch Changes
69 |
70 | - Updated dependencies
71 | - @designcombo/core@0.3.0
72 |
73 | ## 0.0.3
74 |
75 | ### Patch Changes
76 |
77 | - Updated dependencies
78 | - @designcombo/core@0.2.1
79 |
80 | ## 0.0.2
81 |
82 | ### Patch Changes
83 |
84 | - Updated dependencies
85 | - @designcombo/core@0.2.0
86 |
87 | ## 0.0.1
88 |
89 | ### Patch Changes
90 |
91 | - Updated dependencies [9439fb3]
92 | - Updated dependencies [9439fb3]
93 | - @designcombo/scene@0.0.1
94 | - @designcombo/timeline@0.0.1
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Graphic Editor
7 |
8 |
9 |
10 | Graphic Editor application using React and TypeScript.
11 |
12 |
13 | DesignCombo
14 | ·
15 | Discord
16 | ·
17 | X
18 |
19 |
20 |
21 | [](https://github.com/designcombo/react-design-editor)
22 |
23 | ## ⌨️ Development
24 |
25 | Clone locally:
26 |
27 | ```bash
28 | $ git clone git@github.com:designcombo/react-design-editor.git
29 | $ cd react-design-editor
30 | $ pnpm install
31 | $ pnpm dev
32 | ```
33 |
34 | Open your browser and visit http://127.0.0.1:5173 , see more at [Development](https://github.com/designcombo/designcombo/react-design-editor).
35 |
36 | ## 📝 License
37 |
38 | Copyright © 2024 [DesignCombo](https://github.com/designcombo/react-design-editor).
39 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/images/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/designcombo/react-design-editor/00f8720299fd8878e4127e74a3fade6731fb1e0d/images/preview.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
14 |
15 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphic-editor",
3 | "private": true,
4 | "version": "0.0.13",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@hookform/resolvers": "^3.6.0",
14 | "@radix-ui/react-avatar": "^1.0.4",
15 | "@radix-ui/react-dialog": "^1.1.1",
16 | "@radix-ui/react-hover-card": "^1.1.1",
17 | "@radix-ui/react-label": "^2.0.2",
18 | "@radix-ui/react-popover": "^1.0.7",
19 | "@radix-ui/react-scroll-area": "^1.0.5",
20 | "@radix-ui/react-slider": "^1.1.2",
21 | "@radix-ui/react-slot": "^1.0.2",
22 | "@radix-ui/react-tabs": "^1.1.0",
23 | "@radix-ui/react-toggle": "^1.1.0",
24 | "@radix-ui/react-toggle-group": "^1.1.0",
25 | "class-variance-authority": "^0.7.0",
26 | "clsx": "^2.1.1",
27 | "cmdk": "^1.0.0",
28 | "fabric": "^6.3.0",
29 | "hotkeys-js": "^3.13.7",
30 | "lodash": "^4.17.21",
31 | "lucide-react": "^0.378.0",
32 | "nanoid": "^5.0.7",
33 | "react": "^18.2.0",
34 | "react-colorful": "^5.6.1",
35 | "react-dom": "^18.2.0",
36 | "react-hook-form": "^7.51.5",
37 | "rxjs": "^7.8.1",
38 | "tailwind-merge": "^2.3.0",
39 | "tailwindcss-animate": "^1.0.7",
40 | "zod": "^3.23.8",
41 | "zustand": "^4.5.2"
42 | },
43 | "devDependencies": {
44 | "@types/lodash": "^4.17.5",
45 | "@types/node": "^20.12.12",
46 | "@types/react": "^18.2.66",
47 | "@types/react-dom": "^18.2.22",
48 | "@typescript-eslint/eslint-plugin": "^7.2.0",
49 | "@typescript-eslint/parser": "^7.2.0",
50 | "@vitejs/plugin-react": "^4.2.1",
51 | "autoprefixer": "^10.4.19",
52 | "eslint": "^8.57.0",
53 | "eslint-plugin-react-hooks": "^4.6.0",
54 | "eslint-plugin-react-refresh": "^0.4.6",
55 | "postcss": "^8.4.38",
56 | "tailwindcss": "^3.4.3",
57 | "typescript": "^5.2.2",
58 | "vite": "^5.2.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import useDataState from "./store/use-data-state";
3 | import { getCompactFontData } from "./utils/fonts";
4 | import { FONTS } from "./data/fonts";
5 | import { Editor } from "./components/editor";
6 |
7 | function App() {
8 | const { setCompactFonts, setFonts } = useDataState();
9 |
10 | useEffect(() => {
11 | setCompactFonts(getCompactFontData(FONTS));
12 | setFonts(FONTS);
13 | }, []);
14 |
15 | return ;
16 | }
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/src/assets/logo-dark-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/designcombo/react-design-editor/00f8720299fd8878e4127e74a3fade6731fb1e0d/src/assets/logo-dark-full.png
--------------------------------------------------------------------------------
/src/assets/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/designcombo/react-design-editor/00f8720299fd8878e4127e74a3fade6731fb1e0d/src/assets/logo-dark.png
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/canvas/actions.ts:
--------------------------------------------------------------------------------
1 | export const actions = () => {};
2 |
--------------------------------------------------------------------------------
/src/components/canvas/canvas.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import FabricCanvas from "./fabric/canvas";
3 | import Artboard from "./fabric/artboard";
4 | import { ADD_PREFIX, dispatcher, filter } from "@/global";
5 | import { addEventListeners, removeEventListeners } from "./events";
6 | import { createItem } from "./fabric/utils/create-item";
7 | import { EventBusData } from "@/interfaces/rxjs";
8 | import { FabricObject } from "fabric";
9 |
10 | const Canvas = () => {
11 | const canvasElRef = React.useRef(null);
12 | const containerRef = React.useRef(null);
13 | const canvasRef = React.useRef(null);
14 |
15 | useEffect(() => {
16 | const canvasWrapper = containerRef.current as HTMLDivElement;
17 |
18 | const canvasWrapperWidth = canvasWrapper.offsetWidth;
19 | const canvasWrapperHeight = canvasWrapper.offsetHeight;
20 |
21 | FabricObject.ownDefaults.borderColor = "blue";
22 | FabricObject.ownDefaults.cornerColor = "white";
23 | FabricObject.ownDefaults.cornerStrokeColor = "#c0c0c0";
24 | FabricObject.ownDefaults.borderOpacityWhenMoving = 1;
25 | FabricObject.ownDefaults.borderScaleFactor = 1;
26 | FabricObject.ownDefaults.cornerSize = 8;
27 | FabricObject.ownDefaults.cornerStyle = "rect";
28 | FabricObject.ownDefaults.centeredScaling = false;
29 | FabricObject.ownDefaults.centeredRotation = true;
30 | FabricObject.ownDefaults.transparentCorners = false;
31 |
32 | const canvas = new FabricCanvas(canvasElRef.current, {
33 | width: canvasWrapperWidth,
34 | height: canvasWrapperHeight,
35 | preserveObjectStacking: true,
36 | selectionColor: "rgba(52, 152, 219,0.1)",
37 | selectionBorderColor: "rgba(52, 152, 219,1.0)",
38 | });
39 |
40 | addEventListeners(canvas);
41 |
42 | canvasRef.current = canvas;
43 |
44 | const size = {
45 | width: 600,
46 | height: 600,
47 | };
48 |
49 | const artboard = new Artboard({
50 | width: size.width,
51 | height: size.height,
52 | });
53 |
54 | canvas.add(artboard);
55 |
56 | canvas.centerArtboards();
57 |
58 | return () => {
59 | removeEventListeners(canvas);
60 | canvas.dispose();
61 | };
62 | }, []);
63 |
64 | const handleAddRemoveEvents = async (object: EventBusData) => {
65 | const item = await createItem(object);
66 | canvasRef.current?.add(item);
67 | };
68 |
69 | useEffect(() => {
70 | const stateEvents = dispatcher.bus.pipe(
71 | filter(({ key }) => key.startsWith(ADD_PREFIX))
72 | );
73 | const subscription = stateEvents.subscribe((object) => {
74 | handleAddRemoveEvents(object);
75 | });
76 | return () => subscription.unsubscribe();
77 | }, [canvasRef]);
78 |
79 | return (
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default Canvas;
87 |
--------------------------------------------------------------------------------
/src/components/canvas/events.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ActiveSelection,
3 | BasicTransformEvent,
4 | FabricObject,
5 | FabricObjectProps,
6 | ObjectEvents,
7 | SerializedObjectProps,
8 | TPointerEvent,
9 | } from "fabric";
10 | import FabricCanvas from "./fabric/canvas";
11 | import { createTextControls } from "./fabric/utils/controls";
12 | import { getObjectsDetail } from "./fabric/utils/get-object-details";
13 | import { SELECTION_UPDATED, dispatcher } from "@/global";
14 |
15 | type FabricMovingEvent = BasicTransformEvent & {
16 | target: FabricObject<
17 | Partial,
18 | SerializedObjectProps,
19 | ObjectEvents
20 | >;
21 | };
22 |
23 | function onObjectMoving(this: FabricCanvas, e: FabricMovingEvent) {}
24 |
25 | function onSelectionCreated(this: FabricCanvas) {
26 | const objects = this.getActiveObjects();
27 | const selection = this.getActiveObject();
28 |
29 | const layers = getObjectsDetail(objects);
30 |
31 | if (objects.length > 1 && selection instanceof ActiveSelection) {
32 | selection.setControlsVisibility({
33 | mt: false,
34 | mb: false,
35 | mr: false,
36 | ml: false,
37 | });
38 | selection.set({
39 | cornerColor: "#FFFFFF",
40 | cornerStyle: "circle",
41 | borderColor: "rgba(60, 64, 198,1.0)",
42 | cornerStrokeColor: "rgba(60, 64, 198,1.0)",
43 | transparentCorners: false,
44 | borderScaleFactor: 1,
45 | touchCornerSize: 40,
46 | cornerSize: 10,
47 | });
48 | }
49 |
50 | if (objects.length === 1) {
51 | const object = objects[0];
52 |
53 | object.controls = createTextControls();
54 | object.setCoords();
55 | }
56 |
57 | dispatcher.dispatch(SELECTION_UPDATED, {
58 | payload: {
59 | layers,
60 | },
61 | });
62 | }
63 |
64 | export function addEventListeners(canvas: FabricCanvas) {
65 | canvas.on("object:moving", onObjectMoving);
66 | canvas.on("selection:created", onSelectionCreated);
67 | canvas.on("selection:updated", onSelectionCreated);
68 | }
69 |
70 | export function removeEventListeners(canvas: FabricCanvas) {
71 | canvas.off("object:moving", onObjectMoving);
72 | canvas.off("selection:created", onSelectionCreated);
73 | canvas.off("selection:updated", onSelectionCreated);
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/canvas/fabric/artboard.ts:
--------------------------------------------------------------------------------
1 | import { Rect, RectProps } from "fabric";
2 |
3 | class Artboard extends Rect {
4 | static type = "Artboard";
5 | static ownDefaults = {
6 | lockMovementX: true,
7 | lockMovementY: true,
8 | hasControls: false,
9 | fill: "#ffffff",
10 | selectable: false,
11 | absolutePositioned: true,
12 | objectCaching: false,
13 | evented: false,
14 | };
15 |
16 | static getDefaults(): Record {
17 | return {
18 | ...super.getDefaults(),
19 | ...Artboard.ownDefaults,
20 | };
21 | }
22 |
23 | constructor(options: Partial) {
24 | super({
25 | ...options,
26 | ...Artboard.ownDefaults,
27 | });
28 | }
29 | }
30 |
31 | export default Artboard;
32 |
--------------------------------------------------------------------------------
/src/components/canvas/fabric/canvas.ts:
--------------------------------------------------------------------------------
1 | import { Canvas as FabricCanvas } from "fabric";
2 |
3 | class Canvas extends FabricCanvas {
4 | public isFlipCanvas: boolean;
5 | constructor(canvasEl: HTMLCanvasElement, options: any) {
6 | super(canvasEl, options);
7 | }
8 |
9 | public centerArtboards() {
10 | const canvasWidth = this.width;
11 | const canvasHeight = this.height;
12 | const vt = this.viewportTransform;
13 | const requestedSize = this.getRequestedSize();
14 | vt[4] = (canvasWidth - requestedSize.width) / 2;
15 | vt[5] = (canvasHeight - requestedSize.height) / 2;
16 | this.requestRenderAll();
17 | }
18 |
19 | public getRequestedSize() {
20 | const artboards = this.getObjects("Artboard");
21 | const artboardsWidth = artboards[0].width;
22 | let artboardsHeight = 0;
23 | artboards.forEach((artboard) => {
24 | artboardsHeight += artboard.height;
25 | });
26 | return {
27 | width: artboardsWidth,
28 | height: artboardsHeight,
29 | };
30 | }
31 | }
32 |
33 | export default Canvas;
34 |
--------------------------------------------------------------------------------
/src/components/canvas/fabric/utils/controls.ts:
--------------------------------------------------------------------------------
1 | import { Control, controlsUtils } from "fabric";
2 | import { drawCircleIcon, drawRotateIcon, drawVerticalIcon } from "./drawer";
3 | const scaleSkewCursorStyleHandler = controlsUtils.scaleSkewCursorStyleHandler;
4 | const scaleCursorStyleHandler = controlsUtils.scaleCursorStyleHandler;
5 | const scalingEqually = controlsUtils.scalingEqually;
6 | const rotationWithSnapping = controlsUtils.rotationWithSnapping;
7 | const rotationStyleHandler = controlsUtils.rotationStyleHandler;
8 | const changeWidth = controlsUtils.changeWidth;
9 |
10 | interface ObjectControls {
11 | ml: Control;
12 | mr: Control;
13 | mb: Control;
14 | mt: Control;
15 | tl: Control;
16 | tr: Control;
17 | bl: Control;
18 | br: Control;
19 | trr: Control;
20 | brr: Control;
21 | blr: Control;
22 | tlr: Control;
23 | mtr: Control;
24 | }
25 |
26 | export const rotateControl = new Control({
27 | x: 0,
28 | y: 0.5,
29 | actionHandler: rotationWithSnapping,
30 | cursorStyleHandler: rotationStyleHandler,
31 | offsetY: 25,
32 | withConnection: false,
33 | actionName: "rotate",
34 | render: drawRotateIcon,
35 | });
36 |
37 | const rotateIcon = (angle: number) => {
38 | return `url("data:image/svg+xml,") 12 12,auto`;
39 | };
40 |
41 | function noop(...args: T[]): void {
42 | // This function does nothing with the arguments
43 | }
44 |
45 | const getRotateControl = (angle: number): Partial => ({
46 | sizeX: 16,
47 | sizeY: 16,
48 | actionHandler: (eventData, transformData, x, y) => {
49 | transformData.target.canvas?.setCursor(
50 | rotateIcon(transformData.target.angle + angle)
51 | );
52 | return rotationWithSnapping(eventData, transformData, x, y);
53 | },
54 | cursorStyleHandler: (eventData, control, fabricObject) => {
55 | return rotateIcon(fabricObject.angle + angle);
56 | },
57 | render: noop,
58 | actionName: "rotate",
59 | });
60 | export const createTextControls = (): Partial => {
61 | return {
62 | tlr: new Control({
63 | x: -0.5,
64 | y: -0.5,
65 | offsetX: -4,
66 | offsetY: -4,
67 | ...getRotateControl(0),
68 | }),
69 |
70 | trr: new Control({
71 | x: 0.5,
72 | y: -0.5,
73 | offsetX: 4,
74 | offsetY: -4,
75 | ...getRotateControl(90),
76 | }),
77 |
78 | brr: new Control({
79 | x: 0.5,
80 | y: 0.5,
81 | offsetX: 4,
82 | offsetY: 4,
83 | ...getRotateControl(180),
84 | }),
85 |
86 | blr: new Control({
87 | x: -0.5,
88 | y: 0.5,
89 | offsetX: -4,
90 | offsetY: 4,
91 | ...getRotateControl(270),
92 | }),
93 |
94 | mr: new Control({
95 | x: 0.5,
96 | y: 0,
97 | actionHandler: changeWidth,
98 | cursorStyleHandler: scaleSkewCursorStyleHandler,
99 | actionName: "resizing",
100 | render: drawVerticalIcon,
101 | }),
102 | ml: new Control({
103 | x: -0.5,
104 | y: 0,
105 | actionHandler: changeWidth,
106 | cursorStyleHandler: scaleSkewCursorStyleHandler,
107 | actionName: "resizing",
108 | render: drawVerticalIcon,
109 | }),
110 |
111 | tl: new Control({
112 | x: -0.5,
113 | y: -0.5,
114 | cursorStyleHandler: scaleCursorStyleHandler,
115 | actionHandler: scalingEqually,
116 | render: drawCircleIcon,
117 | }),
118 |
119 | tr: new Control({
120 | x: 0.5,
121 | y: -0.5,
122 | cursorStyleHandler: scaleCursorStyleHandler,
123 | actionHandler: scalingEqually,
124 | render: drawCircleIcon,
125 | }),
126 |
127 | bl: new Control({
128 | x: -0.5,
129 | y: 0.5,
130 | cursorStyleHandler: scaleCursorStyleHandler,
131 | actionHandler: scalingEqually,
132 | render: drawCircleIcon,
133 | }),
134 |
135 | br: new Control({
136 | x: 0.5,
137 | y: 0.5,
138 | cursorStyleHandler: scaleCursorStyleHandler,
139 | actionHandler: scalingEqually,
140 | render: drawCircleIcon,
141 | }),
142 | };
143 | };
144 |
--------------------------------------------------------------------------------
/src/components/canvas/fabric/utils/create-item.ts:
--------------------------------------------------------------------------------
1 | import { ADD_IMAGE, ADD_TEXT } from "@/global";
2 | import { IImage, ILayer } from "@/interfaces/editor";
3 | import { EventBusData } from "@/interfaces/rxjs";
4 | import { loadFonts } from "@/utils/fonts";
5 | import { FabricImage, ITextProps, Textbox } from "fabric";
6 |
7 | export const createItem = async (event: EventBusData) => {
8 | if (event.key === ADD_TEXT) {
9 | const layer = event.val.payload as ILayer;
10 | await loadFonts([
11 | {
12 | name: layer.details.fontFamily,
13 | url: layer.details.fontUrl,
14 | },
15 | ]);
16 | const { text, ...options } = layer.details;
17 |
18 | const textbox = new Textbox(text, options as ITextProps);
19 | return textbox;
20 | }
21 |
22 | if (event.key === ADD_IMAGE) {
23 | const layer = event.val.payload as IImage;
24 | const image = await FabricImage.fromURL(layer.details.src);
25 | image.set({
26 | scaleX: 0.1,
27 | scaleY: 0.1,
28 | });
29 | return image;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/canvas/fabric/utils/drawer.ts:
--------------------------------------------------------------------------------
1 | import * as fabric from "fabric";
2 | const horizontalIcon = document.createElement("img");
3 | const verticalIcon = document.createElement("img");
4 | horizontalIcon.src =
5 | "https://ik.imagekit.io/uonadbo34e6/icons/horizontal_7M4-rXo2E.svg";
6 | verticalIcon.src =
7 | "https://ik.imagekit.io/uonadbo34e6/icons/vertical_hEUb9e0-3.svg";
8 |
9 | const largeSide = 45;
10 | const smallSide = 10;
11 |
12 | export function drawVerticalIcon(
13 | ctx: CanvasRenderingContext2D,
14 | left: number,
15 | top: number,
16 | styleOverride: any,
17 | fabricObject: any
18 | ) {
19 | ctx.save();
20 | ctx.translate(left, top);
21 | ctx.rotate(fabric.util.degreesToRadians(90 + fabricObject.angle));
22 | ctx.restore();
23 | }
24 |
25 | export function drawHorizontalIcon(
26 | ctx: CanvasRenderingContext2D,
27 | left: number,
28 | top: number,
29 | styleOverride: any,
30 | fabricObject: any
31 | ) {
32 | // @ts-ignore
33 | const size = this.cornerSize;
34 | ctx.save();
35 | ctx.translate(left, top);
36 | ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
37 | // icon, x, y, width, height
38 |
39 | ctx.drawImage(
40 | horizontalIcon,
41 | false ? -smallSide / 2 : -size / 2, //x
42 | false ? -size / 2 : -smallSide / 2, //y
43 | false ? smallSide : size, // width
44 | false ? size : smallSide // height
45 | );
46 | ctx.restore();
47 | }
48 |
49 | const rotateControlSize = 26;
50 | const img = document.createElement("img");
51 | img.src = "https://ik.imagekit.io/uonadbo34e6/icons/Rotate_qCgLn7Jao.svg";
52 |
53 | export function drawRotateIcon(
54 | ctx: CanvasRenderingContext2D,
55 | left: number,
56 | top: number,
57 | styleOverride: any,
58 | fabricObject: any
59 | ) {
60 | ctx.save();
61 | ctx.translate(left, top);
62 | ctx.shadowBlur = 15;
63 | ctx.shadowOffsetY = 8;
64 | ctx.shadowColor = "rgba(0,0,0,0.08)";
65 | ctx.drawImage(
66 | img,
67 | -rotateControlSize / 2,
68 | -rotateControlSize / 2,
69 | rotateControlSize,
70 | rotateControlSize
71 | );
72 | ctx.restore();
73 | }
74 |
75 | export function drawCircleIcon(
76 | ctx: CanvasRenderingContext2D,
77 | left: number,
78 | top: number,
79 | styleOverride: any,
80 | fabricObject: any
81 | ) {
82 | ctx.save();
83 | ctx.translate(left, top);
84 | ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
85 | ctx.beginPath();
86 | ctx.lineCap = "round";
87 | ctx.lineWidth = 3;
88 | ctx.shadowBlur = 2;
89 | ctx.shadowColor = "black";
90 | ctx.arc(0, 0, 5.5, 0, 2 * Math.PI);
91 | ctx.fillStyle = "#ffffff";
92 | ctx.fill();
93 | ctx.restore();
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/canvas/fabric/utils/get-object-details.ts:
--------------------------------------------------------------------------------
1 | import { FabricObject } from "fabric";
2 |
3 | export const getObjectsDetail = (objects: FabricObject[]) => {
4 | const objectsDetails = [];
5 | objects.forEach((object) => {
6 | objectsDetails.push(getObjectDetails(object));
7 | });
8 | return objectsDetails;
9 | };
10 |
11 | const getObjectDetails = (object: FabricObject) => {
12 | const commonProps = {
13 | type: object.type,
14 | left: object.left,
15 | top: object.top,
16 | width: object.width,
17 | height: object.height,
18 | };
19 | if (object.type === "textbox") {
20 | return {
21 | id: object.id,
22 | type: object.type,
23 | details: {
24 | ...commonProps,
25 | text: object.text,
26 | fontUrl: object.fontUrl,
27 | },
28 | };
29 | }
30 | return {
31 | id: object.id,
32 | type: object.type,
33 | details: {
34 | ...commonProps,
35 | },
36 | };
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/basic-audio.tsx:
--------------------------------------------------------------------------------
1 | const BasicAudio = () => {
2 | return (
3 |
4 |
5 | BasicAudio
6 |
7 |
8 | );
9 | };
10 |
11 | export default BasicAudio;
12 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/basic-image.tsx:
--------------------------------------------------------------------------------
1 | const BasicImage = () => {
2 | return (
3 |
4 |
5 | BasicImage
6 |
7 |
8 | );
9 | };
10 |
11 | export default BasicImage;
12 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/basic-text.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | Popover,
4 | PopoverContent,
5 | PopoverTrigger,
6 | } from "@/components/ui/popover";
7 | import { ScrollArea } from "@/components/ui/scroll-area";
8 | import { DEFAULT_FONT } from "@/data/fonts";
9 | import { ICompactFont, IFont, ILayer } from "@/interfaces/editor";
10 | import useDataState from "@/store/use-data-state";
11 | import { loadFonts } from "@/utils/fonts";
12 | import { EDIT_OBJECT, dispatcher } from "@/global";
13 | import { ChevronDown, Ellipsis, Strikethrough, Underline } from "lucide-react";
14 | import { useEffect, useState } from "react";
15 | import Opacity from "./common/opacity";
16 | import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
17 | import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
18 | import { Input } from "@/components/ui/input";
19 | import Transform from "./common/transform";
20 |
21 | interface ITextControlProps {
22 | color: string;
23 | colorDisplay: string;
24 | fontSize: number;
25 | fontSizeDisplay: string;
26 | fontFamily: string;
27 | fontFamilyDisplay: string;
28 | opacity: number;
29 | opacityDisplay: string;
30 | textAlign: string;
31 | textDecoration: string;
32 | }
33 |
34 | const getStyleNameFromFontName = (fontName: string) => {
35 | const fontFamilyEnd = fontName.lastIndexOf("-");
36 | const styleName = fontName
37 | .substring(fontFamilyEnd + 1)
38 | .replace("Italic", " Italic");
39 | return styleName;
40 | };
41 |
42 | const BasicText = ({ item }: { item: ILayer }) => {
43 | const [properties, setProperties] = useState({
44 | color: "#000000",
45 | colorDisplay: "#000000",
46 | fontSize: 12,
47 | fontSizeDisplay: "12px",
48 | fontFamily: "Open Sans",
49 | fontFamilyDisplay: "Open Sans",
50 | opacity: 1,
51 | opacityDisplay: "100%",
52 | textAlign: "left",
53 | textDecoration: "none",
54 | });
55 |
56 | const [selectedFont, setSelectedFont] = useState({
57 | family: "Open Sans",
58 | styles: [],
59 | default: DEFAULT_FONT,
60 | name: "Regular",
61 | });
62 | const { compactFonts, fonts } = useDataState();
63 |
64 | useEffect(() => {
65 | const fontFamily = item.details.fontFamily || DEFAULT_FONT.postScriptName;
66 | const currentFont = fonts.find(
67 | (font) => font.postScriptName === fontFamily
68 | );
69 | const selectedFont = compactFonts.find(
70 | (font) => font.family === currentFont?.family
71 | );
72 |
73 | setSelectedFont({
74 | ...selectedFont,
75 | name: getStyleNameFromFontName(currentFont.postScriptName),
76 | });
77 |
78 | if (item.details.opacityDisplay == undefined) {
79 | item.details.opacityDisplay = "100";
80 | }
81 |
82 | if (item.details.fontSizeDisplay == undefined) {
83 | item.details.fontSizeDisplay = "62";
84 | }
85 | setProperties({
86 | color: item.details.color || "#ffffff",
87 | colorDisplay: item.details.color || "#ffffff",
88 | fontSize: item.details.fontSize || 62,
89 | fontSizeDisplay: (item.details.fontSize || 62) + "px",
90 | fontFamily: selectedFont?.family || "Open Sans",
91 | fontFamilyDisplay: selectedFont?.family || "Open Sans",
92 | opacity: item.details.opacity || 1,
93 | opacityDisplay: (item.details.opacityDisplay || "100") + "%",
94 | textAlign: item.details.textAlign || "left",
95 | textDecoration: item.details.textDecoration || "none",
96 | });
97 | }, [item.id]);
98 |
99 | const handleChangeFont = async (font: ICompactFont) => {
100 | const fontName = font.default.postScriptName;
101 | const fontUrl = font.default.url;
102 |
103 | await loadFonts([
104 | {
105 | name: fontName,
106 | url: fontUrl,
107 | },
108 | ]);
109 | setSelectedFont({ ...font, name: getStyleNameFromFontName(fontName) });
110 | setProperties({
111 | ...properties,
112 | fontFamily: font.default.family,
113 | fontFamilyDisplay: font.default.family,
114 | });
115 |
116 | dispatcher.dispatch(EDIT_OBJECT, {
117 | payload: {
118 | details: {
119 | fontFamily: fontName,
120 | fontUrl: fontUrl,
121 | },
122 | },
123 | });
124 | };
125 |
126 | const handleChangeFontStyle = async (font: IFont) => {
127 | const fontName = font.postScriptName;
128 | const fontUrl = font.url;
129 | const styleName = getStyleNameFromFontName(fontName);
130 | await loadFonts([
131 | {
132 | name: fontName,
133 | url: fontUrl,
134 | },
135 | ]);
136 | setSelectedFont({ ...selectedFont, name: styleName });
137 | dispatcher.dispatch(EDIT_OBJECT, {
138 | payload: {
139 | details: {
140 | fontFamily: fontName,
141 | fontUrl: fontUrl,
142 | },
143 | },
144 | });
145 | };
146 |
147 | return (
148 |
149 |
150 | Text
151 |
152 |
153 |
154 |
155 |
156 |
160 |
161 |
162 |
166 |
167 |
168 |
169 | px
170 |
171 |
172 |
173 |
174 |
178 |
179 |
180 |
Style
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | );
196 | };
197 |
198 | const Fill = () => {
199 | return (
200 |
207 |
Fill
208 |
211 |
212 |
215 |
216 |
217 | );
218 | };
219 |
220 | const Stroke = () => {
221 | return (
222 |
229 |
Stroke
230 |
233 |
234 |
237 |
238 |
239 | );
240 | };
241 | const Shadow = () => {
242 | return (
243 |
250 |
Shadow
251 |
254 |
255 |
258 |
259 |
260 | );
261 | };
262 | const Background = () => {
263 | return (
264 |
271 |
Background
272 |
275 |
276 |
279 |
280 |
281 | );
282 | };
283 |
284 | const FontFamily = ({
285 | handleChangeFont,
286 | fontFamilyDisplay,
287 | }: {
288 | handleChangeFont: (font: ICompactFont) => void;
289 | fontFamilyDisplay: string;
290 | }) => {
291 | const { compactFonts } = useDataState();
292 |
293 | return (
294 |
295 |
296 |
306 |
307 |
308 |
309 | {compactFonts.map((font, index) => (
310 | handleChangeFont(font)}
312 | className="hover:bg-zinc-800/50 cursor-pointer px-2 py-1"
313 | key={index}
314 | >
315 |

322 |
323 | ))}
324 |
325 |
326 |
327 | );
328 | };
329 |
330 | const FontStyle = ({
331 | selectedFont,
332 | handleChangeFontStyle,
333 | }: {
334 | selectedFont: ICompactFont;
335 | handleChangeFontStyle: (font: IFont) => void;
336 | }) => {
337 | return (
338 |
339 |
340 |
350 |
351 |
352 |
353 | {selectedFont.styles.map((style, index) => {
354 | const fontFamilyEnd = style.postScriptName.lastIndexOf("-");
355 | const styleName = style.postScriptName
356 | .substring(fontFamilyEnd + 1)
357 | .replace("Italic", " Italic");
358 | return (
359 | handleChangeFontStyle(style)}
363 | >
364 | {styleName}
365 |
366 | );
367 | })}
368 |
369 |
370 | );
371 | };
372 | const TextDecoration = () => {
373 | const [value, setValue] = useState(["left"]);
374 | const onChangeAligment = (value: string[]) => {
375 | setValue(value);
376 | };
377 | return (
378 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
407 |
408 |
409 | );
410 | };
411 |
412 | const Alignment = () => {
413 | const [value, setValue] = useState("left");
414 | const onChangeAligment = (value: string) => {
415 | setValue(value);
416 | };
417 | return (
418 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 | );
436 | };
437 |
438 | export default BasicText;
439 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/common/opacity.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import { Input } from '@/components/ui/input';
3 | import { Label } from '@/components/ui/label';
4 |
5 | import { Slider } from '@/components/ui/slider';
6 | import { Plus, RefreshCcw, RotateCw } from 'lucide-react';
7 | import { useState } from 'react';
8 |
9 | const Opacity = () => {
10 | const [value, setValue] = useState([10]);
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
25 |
32 |
33 |
34 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default Opacity;
49 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/common/transform.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import { Input } from '@/components/ui/input';
3 | import { Slider } from '@/components/ui/slider';
4 | import { RotateCw } from 'lucide-react';
5 | import { useState } from 'react';
6 |
7 | const Transform = () => {
8 | const [value, setValue] = useState([10]);
9 |
10 | return (
11 |
12 |
Transform
13 |
14 |
Scale
15 |
22 |
29 |
30 |
31 |
38 |
39 |
40 |
41 |
42 |
43 |
Position
44 |
51 |
52 |
53 |
54 | x
55 |
56 |
57 |
58 |
59 |
60 | y
61 |
62 |
63 |
64 |
65 |
72 |
73 |
74 |
75 |
76 |
77 |
Rotate
78 |
85 |
86 |
87 |
88 |
95 |
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default Transform;
103 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/control-item.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useLayoutStore from "@/store/use-layout-store";
3 | import { useEffect, useState } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { X } from "lucide-react";
6 | import Presets from "./presets";
7 | import Smart from "./smart";
8 | import BasicText from "./basic-text";
9 | import BasicImage from "./basic-image";
10 | import { ILayer } from "@/interfaces/editor";
11 | import {
12 | SELECTION_PREFIX,
13 | SELECTION_UPDATED,
14 | dispatcher,
15 | filter,
16 | } from "@/global";
17 |
18 | const Container = ({ children }: { children: React.ReactNode }) => {
19 | const { activeToolboxItem } = useLayoutStore();
20 | const [item, setItem] = useState(null);
21 | const [displayToolbox, setDisplayToolbox] = useState(false);
22 |
23 | useEffect(() => {
24 | const stateEvents = dispatcher.bus.pipe(
25 | filter(({ key }) => key.startsWith(SELECTION_PREFIX))
26 | );
27 | const subscription = stateEvents.subscribe((obj) => {
28 | if (obj.key === SELECTION_UPDATED) {
29 | const layers = obj.val.payload.layers;
30 | if (layers.length === 1) {
31 | const [layer] = layers;
32 | setItem(layer);
33 | } else {
34 | setItem(null);
35 | setDisplayToolbox(false);
36 | }
37 | }
38 | });
39 | return () => subscription.unsubscribe();
40 | }, []);
41 |
42 | useEffect(() => {
43 | if (activeToolboxItem) {
44 | setDisplayToolbox(true);
45 | } else {
46 | setDisplayToolbox(false);
47 | }
48 | }, [activeToolboxItem]);
49 |
50 | if (!item) {
51 | return null;
52 | }
53 |
54 | return (
55 |
63 |
64 |
71 | {React.cloneElement(children as React.ReactElement
, {
72 | item,
73 | activeToolboxItem,
74 | })}
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | const ActiveControlItem = ({
82 | item,
83 | activeToolboxItem,
84 | }: {
85 | item?: ILayer;
86 | activeToolboxItem?: string;
87 | }) => {
88 | if (!item || !activeToolboxItem) {
89 | return null;
90 | }
91 | return (
92 | <>
93 | {
94 | {
95 | "basic-textbox": ,
96 | "basic-image": ,
97 | "preset-textbox": ,
98 | smart: ,
99 | }[activeToolboxItem]
100 | }
101 | >
102 | );
103 | };
104 |
105 | export const ControlItem = () => {
106 | return (
107 |
108 |
109 |
110 | );
111 | };
112 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/index.tsx:
--------------------------------------------------------------------------------
1 | export { ControlItem } from './control-item';
2 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/presets.tsx:
--------------------------------------------------------------------------------
1 | const Presets = () => {
2 | return (
3 |
8 | );
9 | };
10 |
11 | export default Presets;
12 |
--------------------------------------------------------------------------------
/src/components/editor/control-item/smart.tsx:
--------------------------------------------------------------------------------
1 | const Smart = () => {
2 | return (
3 |
4 |
5 | Ai things
6 |
7 |
8 | );
9 | };
10 |
11 | export default Smart;
12 |
--------------------------------------------------------------------------------
/src/components/editor/control-list.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 | import { Icons } from "@/components/shared/icons";
3 | import { Button } from "@/components/ui/button";
4 | import useLayoutStore from "@/store/use-layout-store";
5 | import {
6 | SELECTION_PREFIX,
7 | SELECTION_UPDATED,
8 | dispatcher,
9 | filter,
10 | } from "@/global";
11 |
12 | type ItemType = any;
13 | export default function ControlList() {
14 | const [controlType, setControlType] = useState(null);
15 |
16 | // useEffect(() => {
17 | // if (activeIds.length === 1) {
18 | // const [id] = activeIds;
19 | // const item = itemsMap[id];
20 | // setControlType(item.type);
21 | // } else {
22 | // setControlType(null);
23 | // }
24 | // }, [activeIds, itemsMap]);
25 |
26 | useEffect(() => {
27 | const stateEvents = dispatcher.bus.pipe(
28 | filter(({ key }) => key.startsWith(SELECTION_PREFIX))
29 | );
30 | const subscription = stateEvents.subscribe((obj) => {
31 | if (obj.key === SELECTION_UPDATED) {
32 | const layers = obj.val.payload.layers;
33 | if (layers.length === 1) {
34 | const [layer] = layers;
35 | setControlType(layer.type);
36 | } else {
37 | setControlType(null);
38 | }
39 | }
40 | });
41 | return () => subscription.unsubscribe();
42 | }, []);
43 |
44 | return <>{controlType && }>;
45 | }
46 |
47 | function ControlMenu({ controlType }: { controlType: ItemType }) {
48 | const { setShowToolboxItem, setActiveToolboxItem, activeToolboxItem } =
49 | useLayoutStore();
50 |
51 | const openToolboxItem = useCallback(
52 | (type: string) => {
53 | if (type === activeToolboxItem) {
54 | setShowToolboxItem(false);
55 | setActiveToolboxItem(null);
56 | } else {
57 | setShowToolboxItem(true);
58 | setActiveToolboxItem(type);
59 | }
60 | },
61 | [activeToolboxItem]
62 | );
63 |
64 | return (
65 |
69 | {
70 | {
71 | image: (
72 |
76 | ),
77 | textbox: (
78 |
82 | ),
83 | }[controlType]
84 | }
85 |
86 | );
87 | }
88 |
89 | const ImageMenuList = ({
90 | openToolboxItem,
91 | type,
92 | }: {
93 | openToolboxItem: (type: string) => void;
94 | type: ItemType;
95 | }) => {
96 | return (
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | const TextMenuList = ({
105 | openToolboxItem,
106 | type,
107 | }: {
108 | openToolboxItem: (type: string) => void;
109 | type: ItemType;
110 | }) => {
111 | return (
112 |
117 | );
118 | };
119 |
120 | const PresetsMenuListItem = ({
121 | openToolboxItem,
122 | type,
123 | }: {
124 | openToolboxItem: (type: string) => void;
125 | type: ItemType;
126 | }) => {
127 | return (
128 |
135 | );
136 | };
137 |
138 | const BasicMenuListItem = ({
139 | openToolboxItem,
140 | type,
141 | }: {
142 | openToolboxItem: (type: string) => void;
143 | type: string;
144 | }) => {
145 | const Icon = Icons[type];
146 | return (
147 |
154 | );
155 | };
156 |
157 | const SmartMenuListItem = ({
158 | openToolboxItem,
159 | }: {
160 | openToolboxItem: (type: string) => void;
161 | }) => {
162 | return (
163 |
170 | );
171 | };
172 |
--------------------------------------------------------------------------------
/src/components/editor/editor.tsx:
--------------------------------------------------------------------------------
1 | import Navbar from "./navbar";
2 | import MenuList from "./menu-list";
3 | import { MenuItem } from "./menu-item";
4 | import ControlList from "./control-list";
5 | import { ControlItem } from "./control-item";
6 |
7 | import useHotkeys from "./use-hotkeys";
8 | import { useEffect } from "react";
9 | import { getCompactFontData } from "@/utils/fonts";
10 | import { FONTS } from "@/data/fonts";
11 | import useDataState from "@/store/use-data-state";
12 | import Canvas from "../canvas/canvas";
13 | import Footer from "./footer";
14 |
15 | const Editor = () => {
16 | const { setCompactFonts, setFonts } = useDataState();
17 |
18 | useHotkeys();
19 |
20 | useEffect(() => {
21 | setCompactFonts(getCompactFontData(FONTS));
22 | setFonts(FONTS);
23 | }, []);
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default Editor;
41 |
--------------------------------------------------------------------------------
/src/components/editor/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 |
3 | import { Command, Info, MinusCircle, PlusCircle } from "lucide-react";
4 | import { Input } from "../ui/input";
5 | import { Slider } from "../ui/slider";
6 |
7 | export default function Footer() {
8 | return (
9 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/editor/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Editor } from './editor';
2 |
--------------------------------------------------------------------------------
/src/components/editor/menu-item/elements.tsx:
--------------------------------------------------------------------------------
1 | export const Elements = () => {
2 | return (
3 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/editor/menu-item/images.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { ScrollArea } from "@/components/ui/scroll-area";
3 | import { IMAGES } from "@/data/images";
4 | import { ADD_IMAGE, dispatcher } from "@/global";
5 | import { nanoid } from "nanoid";
6 |
7 | export const Images = () => {
8 | const handleAddImage = (src: string) => {
9 | dispatcher?.dispatch(ADD_IMAGE, {
10 | payload: {
11 | id: nanoid(),
12 | details: {
13 | src: src,
14 | },
15 | },
16 | options: {
17 | trackId: "main",
18 | },
19 | });
20 | };
21 |
22 | return (
23 |
24 |
25 | Photos
26 |
27 |
28 |
29 | {IMAGES.map((image, index) => {
30 | return (
31 |
handleAddImage(image.src)}
33 | key={index}
34 | className="flex items-center justify-center w-full bg-zinc-950 pb-2 overflow-hidden cursor-pointer"
35 | >
36 |
})
41 |
42 | );
43 | })}
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | function modifyImageUrl(url: string): string {
51 | const uploadIndex = url.indexOf("/upload");
52 | if (uploadIndex === -1) {
53 | throw new Error("Invalid URL: /upload not found");
54 | }
55 |
56 | const modifiedUrl =
57 | url.slice(0, uploadIndex + 7) +
58 | "/w_0.05,c_scale" +
59 | url.slice(uploadIndex + 7);
60 | return modifiedUrl;
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/editor/menu-item/index.tsx:
--------------------------------------------------------------------------------
1 | export { MenuItem } from './menu-item';
2 |
--------------------------------------------------------------------------------
/src/components/editor/menu-item/menu-item.tsx:
--------------------------------------------------------------------------------
1 | import useLayoutStore from "@/store/use-layout-store";
2 | import { Texts } from "./texts";
3 | import { Uploads } from "./uploads";
4 | import { Elements } from "./elements";
5 | import { Images } from "./images";
6 | import { X } from "lucide-react";
7 | import { Button } from "@/components/ui/button";
8 |
9 | const Container = ({ children }: { children: React.ReactNode }) => {
10 | const { showMenuItem, setShowMenuItem } = useLayoutStore();
11 | return (
12 |
20 |
21 |
22 |
29 | {children}
30 |
31 |
32 | );
33 | };
34 |
35 | const ActiveMenuItem = () => {
36 | const { activeMenuItem } = useLayoutStore();
37 |
38 | if (activeMenuItem === "texts") {
39 | return ;
40 | }
41 | if (activeMenuItem === "shapes") {
42 | return ;
43 | }
44 |
45 | if (activeMenuItem === "images") {
46 | return ;
47 | }
48 | if (activeMenuItem === "uploads") {
49 | return ;
50 | }
51 | return null;
52 | };
53 |
54 | export const MenuItem = () => {
55 | return (
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/editor/menu-item/texts.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { DEFAULT_FONT } from "@/data/fonts";
3 | import { ADD_TEXT, dispatcher } from "@/global";
4 | import { nanoid } from "nanoid";
5 |
6 | export const Texts = () => {
7 | const handleAddText = () => {
8 | dispatcher?.dispatch(ADD_TEXT, {
9 | payload: {
10 | id: nanoid(),
11 | details: {
12 | text: "Add text",
13 | fontSize: 32,
14 | fontFamily: DEFAULT_FONT.postScriptName,
15 | fontUrl: DEFAULT_FONT.url,
16 | width: 180,
17 | textAlign: "center",
18 | fill: "#111111",
19 | },
20 | },
21 | options: {},
22 | });
23 | };
24 |
25 | return (
26 |
27 |
28 | Text
29 |
30 |
31 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/editor/menu-item/uploads.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | ADD_AUDIO,
4 | ADD_IMAGE,
5 | ADD_TEXT,
6 | ADD_VIDEO,
7 | dispatcher,
8 | } from "@/global";
9 | import { nanoid } from "nanoid";
10 | import { IMAGES } from "@/data/images";
11 | import { DEFAULT_FONT } from "@/data/fonts";
12 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
13 | import { UploadIcon } from "lucide-react";
14 |
15 | export const Uploads = () => {
16 | const handleAddImage = () => {
17 | dispatcher?.dispatch(ADD_IMAGE, {
18 | payload: {
19 | id: nanoid(),
20 | details: {
21 | src: IMAGES[4].src,
22 | },
23 | },
24 | options: {
25 | trackId: "main",
26 | },
27 | });
28 | };
29 |
30 | const handleAddText = () => {
31 | dispatcher?.dispatch(ADD_TEXT, {
32 | payload: {
33 | id: nanoid(),
34 | details: {
35 | text: "Heading",
36 | fontSize: 200,
37 | width: 900,
38 | fontUrl: DEFAULT_FONT.url,
39 | fontFamily: DEFAULT_FONT.postScriptName,
40 | color: "#ffffff",
41 | WebkitTextStrokeColor: "green",
42 | WebkitTextStrokeWidth: "20px",
43 | textShadow: "30px 30px 100px rgba(255, 255, 0, 1)",
44 | wordWrap: "break-word",
45 | wordBreak: "break-all",
46 | },
47 | },
48 | options: {},
49 | });
50 | };
51 |
52 | const handleAddAudio = () => {
53 | dispatcher?.dispatch(ADD_AUDIO, {
54 | payload: {
55 | id: nanoid(),
56 | details: {
57 | src: "https://ik.imagekit.io/snapmotion/timer-voice.mp3",
58 | },
59 | },
60 | options: {},
61 | });
62 | };
63 |
64 | const handleAddVideo = () => {
65 | dispatcher?.dispatch(ADD_VIDEO, {
66 | payload: {
67 | id: nanoid(),
68 | details: {
69 | src: "https://ik.imagekit.io/snapmotion/75475-556034323_medium.mp4",
70 | },
71 | metadata: {
72 | resourceId: "7415538a-5d61-4a81-ad79-c00689b6cc10",
73 | },
74 | },
75 | options: {
76 | trackId: "main",
77 | },
78 | });
79 | };
80 |
81 | const handleAddVideo2 = () => {
82 | dispatcher?.dispatch(ADD_VIDEO, {
83 | payload: {
84 | id: nanoid(),
85 | details: {
86 | src: "https://ik.imagekit.io/snapmotion/flat.mp4",
87 | },
88 | metadata: {
89 | resourceId: "7415538a-5do1-4m81-a279-c00689b6cc10",
90 | },
91 | },
92 | });
93 | };
94 | return (
95 |
96 |
97 | Your media
98 |
99 |
100 |
101 |
102 |
103 | Project
104 | Workspace
105 |
106 |
107 |
115 |
116 |
117 |
118 |
125 | Some assets
126 |
127 |
128 |
129 |
130 |
131 | );
132 | };
133 |
--------------------------------------------------------------------------------
/src/components/editor/menu-list.tsx:
--------------------------------------------------------------------------------
1 | import useLayoutStore from "@/store/use-layout-store";
2 | import { Icons } from "@/components/shared/icons";
3 | import { Button } from "@/components/ui/button";
4 | import { cn } from "@/lib/utils";
5 |
6 | export default function MenuList() {
7 | const { setActiveMenuItem, setShowMenuItem, activeMenuItem, showMenuItem } =
8 | useLayoutStore();
9 | return (
10 |
14 |
29 |
44 |
45 |
60 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/editor/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | DESIGN_RESIZE,
4 | HISTORY_UNDO,
5 | HISTORY_REDO,
6 | dispatcher,
7 | } from "@/global";
8 | import logoDark from "@/assets/logo-dark.png";
9 | import { Icons } from "../shared/icons";
10 | import {
11 | Popover,
12 | PopoverContent,
13 | PopoverTrigger,
14 | } from "@/components/ui/popover";
15 | import { Download } from "lucide-react";
16 |
17 | export default function Navbar() {
18 | const handleUndo = () => {
19 | dispatcher.dispatch(HISTORY_UNDO);
20 | };
21 |
22 | const handleRedo = () => {
23 | dispatcher.dispatch(HISTORY_REDO);
24 | };
25 |
26 | const handleExport = () => {};
27 | const openLink = (url: string) => {
28 | window.open(url, "_blank"); // '_blank' will open the link in a new tab
29 | };
30 |
31 | return (
32 |
39 |
40 |
41 |

42 |
43 |
44 |
52 |
60 |
61 |
62 |
63 |
64 |
65 |
Untitled graphic
66 |
67 |
68 |
69 |
70 |
71 |
72 |
90 |
98 |
99 |
100 |
101 | );
102 | }
103 |
104 | interface ResizeOptionProps {
105 | label: string;
106 | icon: string;
107 | value: ResizeValue;
108 | }
109 | interface ResizeValue {
110 | width: number;
111 | height: number;
112 | name: string;
113 | }
114 | const RESIZE_OPTIONS: ResizeOptionProps[] = [
115 | {
116 | label: "16:9",
117 | icon: "landscape",
118 | value: {
119 | width: 1920,
120 | height: 1080,
121 | name: "16:9",
122 | },
123 | },
124 | {
125 | label: "9:16",
126 | icon: "portrait",
127 | value: {
128 | width: 1080,
129 | height: 1920,
130 | name: "9:16",
131 | },
132 | },
133 | {
134 | label: "1:1",
135 | icon: "square",
136 | value: {
137 | width: 1080,
138 | height: 1080,
139 | name: "1:1",
140 | },
141 | },
142 | ];
143 |
144 | const ResizeVideo = () => {
145 | const handleResize = (payload: ResizeValue) => {
146 | dispatcher.dispatch(DESIGN_RESIZE, {
147 | payload,
148 | });
149 | };
150 | return (
151 |
152 |
153 |
160 |
161 |
162 |
163 | {RESIZE_OPTIONS.map((option, index) => (
164 |
171 | ))}
172 |
173 |
174 |
175 | );
176 | };
177 |
178 | const ResizeOption = ({
179 | label,
180 | icon,
181 | value,
182 | handleResize,
183 | }: ResizeOptionProps & { handleResize: (payload: ResizeValue) => void }) => {
184 | const Icon = Icons[icon];
185 | return (
186 | handleResize(value)}
188 | className="flex items-center gap-4 hover:bg-zinc-50/10 cursor-pointer"
189 | >
190 |
191 |
192 |
193 |
194 |
{label}
195 |
Tiktok, Instagram
196 |
197 |
198 | );
199 | };
200 |
--------------------------------------------------------------------------------
/src/components/editor/resize-video.tsx:
--------------------------------------------------------------------------------
1 | const ResizeVideo = () => {
2 | return ResizeVideo
;
3 | };
4 |
--------------------------------------------------------------------------------
/src/components/editor/use-hotkeys.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_FONT } from "@/data/fonts";
2 | import {
3 | ACTIVE_CLONE,
4 | ACTIVE_DELETE,
5 | ADD_TEXT,
6 | HISTORY_REDO,
7 | HISTORY_UNDO,
8 | LAYER_SELECT,
9 | PLAYER_SEEK_BY,
10 | PLAYER_TOGGLE_PLAY,
11 | dispatcher,
12 | } from "@/global";
13 | import hotkeys from "hotkeys-js";
14 | import { nanoid } from "nanoid";
15 | import { useEffect } from "react";
16 |
17 | const useHotkeys = () => {
18 | useEffect(() => {
19 | const dispatch = dispatcher.dispatch;
20 | // handle undo
21 | hotkeys("ctrl+z,command+z", (event) => {
22 | event.preventDefault(); // Prevent the default action
23 | dispatch(HISTORY_UNDO);
24 | // dispatch(UNDO);
25 | });
26 |
27 | // handle redo: ctrl+shift+z
28 | hotkeys("ctrl+shift+z,command+shift+z", (event) => {
29 | event.preventDefault(); // Prevent the default action
30 | // redo();
31 | dispatch(HISTORY_REDO);
32 | });
33 |
34 | // Define the shortcut and corresponding action
35 | hotkeys("ctrl+s,command+s", (event) => {
36 | event.preventDefault(); // Prevent the default action
37 | console.log("split action");
38 | // dispatch(ACTIVE_SPLIT);
39 | });
40 |
41 | // duplicate item
42 | hotkeys("ctrl+d,command+d", (event) => {
43 | event.preventDefault(); // Prevent the default action
44 | dispatch(ACTIVE_CLONE);
45 | });
46 |
47 | hotkeys("backspace,delete", (event) => {
48 | event.preventDefault(); // Prevent the default action
49 | dispatch(ACTIVE_DELETE);
50 | });
51 |
52 | hotkeys("esc", (event) => {
53 | event.preventDefault(); // Prevent the default action
54 | dispatcher.dispatch(LAYER_SELECT, { payload: { activeIds: [] } });
55 | });
56 |
57 | hotkeys("space", (event) => {
58 | event.preventDefault();
59 | dispatch(PLAYER_TOGGLE_PLAY);
60 | });
61 |
62 | hotkeys("down", (event) => {
63 | event.preventDefault();
64 | dispatch(PLAYER_SEEK_BY, { payload: { frames: 1 } });
65 | });
66 |
67 | hotkeys("up", (event) => {
68 | event.preventDefault();
69 | dispatch(PLAYER_SEEK_BY, { payload: { frames: -1 } });
70 | });
71 |
72 | // New shortcut for the 'T' key
73 | hotkeys("t", (event) => {
74 | dispatcher.dispatch(ADD_TEXT, {
75 | payload: {
76 | id: nanoid(),
77 | details: {
78 | text: "Add text",
79 | fontSize: 32,
80 | fontFamily: DEFAULT_FONT.postScriptName,
81 | fontUrl: DEFAULT_FONT.url,
82 | width: 180,
83 | textAlign: "center",
84 | fill: "#111111",
85 | },
86 | },
87 | });
88 | });
89 |
90 | return () => {
91 | hotkeys.unbind("ctrl+shift+z,command+shift+z");
92 | hotkeys.unbind("ctrl+z,command+z");
93 | hotkeys.unbind("ctrl+s,command+s");
94 | hotkeys.unbind("ctrl+d,command+d");
95 | hotkeys.unbind("backspace,delete");
96 | hotkeys.unbind("escape");
97 | hotkeys.unbind("down");
98 | hotkeys.unbind("up");
99 | hotkeys.unbind("space");
100 | hotkeys.unbind("t");
101 | };
102 | }, []);
103 | };
104 |
105 | export default useHotkeys;
106 |
--------------------------------------------------------------------------------
/src/components/shared/icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertTriangle,
3 | ArrowRight,
4 | ArrowUpRight,
5 | BookOpen,
6 | Check,
7 | ChevronLeft,
8 | ChevronRight,
9 | Copy,
10 | CreditCard,
11 | File,
12 | FileText,
13 | FolderClosed,
14 | HelpCircle,
15 | Home,
16 | Image,
17 | Laptop,
18 | LayoutPanelLeft,
19 | LineChart,
20 | Loader2,
21 | LucideIcon,
22 | LucideProps,
23 | MessagesSquare,
24 | Moon,
25 | MoreVertical,
26 | Package,
27 | Plus,
28 | Puzzle,
29 | Search,
30 | Settings,
31 | SunMedium,
32 | Trash,
33 | Text,
34 | Type,
35 | User,
36 | X,
37 | Square,
38 | RectangleVertical,
39 | RectangleHorizontal,
40 | WandSparkles,
41 | Zap,
42 | Music,
43 | VideoIcon,
44 | } from "lucide-react";
45 |
46 | export type Icon = LucideIcon;
47 |
48 | export const Icons = {
49 | add: Plus,
50 | audio: Music,
51 | arrowRight: ArrowRight,
52 | arrowUpRight: ArrowUpRight,
53 | billing: CreditCard,
54 | bookOpen: BookOpen,
55 | chevronLeft: ChevronLeft,
56 | chevronRight: ChevronRight,
57 | check: Check,
58 | close: X,
59 | copy: Copy,
60 | dashboard: LayoutPanelLeft,
61 | ellipsis: MoreVertical,
62 | folder: FolderClosed,
63 | gitHub: ({ ...props }: LucideProps) => (
64 |
79 | ),
80 | google: ({ ...props }: LucideProps) => (
81 |
96 | ),
97 | nextjs: ({ ...props }: LucideProps) => (
98 |
113 | ),
114 | help: HelpCircle,
115 | home: Home,
116 | image: Image,
117 | landscape: RectangleHorizontal,
118 | laptop: Laptop,
119 | lineChart: LineChart,
120 | logo: Puzzle,
121 | media: Image,
122 | messages: MessagesSquare,
123 | moon: Moon,
124 | package: Package,
125 | page: File,
126 | portrait: RectangleVertical,
127 | post: FileText,
128 | preset: Zap,
129 | search: Search,
130 | square: Square,
131 | redo: ({ ...props }: LucideProps) => (
132 |
145 | ),
146 | shapes: ({ ...props }: LucideProps) => (
147 |
165 | ),
166 | settings: Settings,
167 | smart: WandSparkles,
168 | spinner: Loader2,
169 | sun: SunMedium,
170 | templates: ({ ...props }: LucideProps) => (
171 |
189 | ),
190 | text: Type,
191 | textbox: Type,
192 |
193 | trash: Trash,
194 | twitter: ({ ...props }: LucideProps) => (
195 |
210 | ),
211 | type: Type,
212 | undo: ({ ...props }: LucideProps) => (
213 |
226 | ),
227 | upload: ({ ...props }: LucideProps) => (
228 |
250 | ),
251 | user: User,
252 | video: VideoIcon,
253 | warning: AlertTriangle,
254 | };
255 |
--------------------------------------------------------------------------------
/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from 'react';
2 |
3 | type Theme = 'dark' | 'light' | 'system';
4 |
5 | type ThemeProviderProps = {
6 | children: React.ReactNode;
7 | defaultTheme?: Theme;
8 | storageKey?: string;
9 | };
10 |
11 | type ThemeProviderState = {
12 | theme: Theme;
13 | setTheme: (theme: Theme) => void;
14 | };
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: 'system',
18 | setTheme: () => null,
19 | };
20 |
21 | const ThemeProviderContext = createContext(initialState);
22 |
23 | export function ThemeProvider({
24 | children,
25 | defaultTheme = 'system',
26 | storageKey = 'vite-ui-theme',
27 | ...props
28 | }: ThemeProviderProps) {
29 | const [theme, setTheme] = useState(
30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
31 | );
32 |
33 | useEffect(() => {
34 | const root = window.document.documentElement;
35 |
36 | root.classList.remove('light', 'dark');
37 |
38 | if (theme === 'system') {
39 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
40 | .matches
41 | ? 'dark'
42 | : 'light';
43 |
44 | root.classList.add(systemTheme);
45 | return;
46 | }
47 |
48 | root.classList.add(theme);
49 | }, [theme]);
50 |
51 | const value = {
52 | theme,
53 | setTheme: (theme: Theme) => {
54 | localStorage.setItem(storageKey, theme);
55 | setTheme(theme);
56 | },
57 | };
58 |
59 | return (
60 |
61 | {children}
62 |
63 | );
64 | }
65 |
66 | export const useTheme = () => {
67 | const context = useContext(ThemeProviderContext);
68 |
69 | if (context === undefined)
70 | throw new Error('useTheme must be used within a ThemeProvider');
71 |
72 | return context;
73 | };
74 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = AvatarPrimitive.Root.displayName
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47 |
48 | export { Avatar, AvatarImage, AvatarFallback }
49 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | xs: 'h-8 rounded-md px-3',
25 | sm: 'h-9 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'h-10 w-10',
28 | },
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default',
33 | },
34 | },
35 | );
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean;
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button';
46 | return (
47 |
52 | );
53 | },
54 | );
55 | Button.displayName = 'Button';
56 |
57 | export { Button, buttonVariants };
58 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { type DialogProps } from "@radix-ui/react-dialog"
3 | import { Command as CommandPrimitive } from "cmdk"
4 | import { Search } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { Dialog, DialogContent } from "@/components/ui/dialog"
8 |
9 | const Command = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 | ))
22 | Command.displayName = CommandPrimitive.displayName
23 |
24 | interface CommandDialogProps extends DialogProps {}
25 |
26 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const HoverCard = HoverCardPrimitive.Root
7 |
8 | const HoverCardTrigger = HoverCardPrimitive.Trigger
9 |
10 | const HoverCardContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
24 | ))
25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
26 |
27 | export { HoverCard, HoverCardTrigger, HoverCardContent }
28 |
--------------------------------------------------------------------------------
/src/components/ui/input-color.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Input } from "./input";
3 | interface InputColorProps {
4 | className?: string;
5 | placeholder?: string;
6 | size?: "sm" | "lg" | "default" | "xs";
7 | value?: string;
8 | onChange?: (value: string) => void;
9 | }
10 | const InputColor = ({
11 | className,
12 | placeholder,
13 | size = "xs",
14 | value = "#44bd32",
15 | onChange,
16 | }: InputColorProps) => {
17 | return (
18 |
19 |
onChange && onChange(e.target.value)}
24 | />
25 |
32 |
33 | );
34 | };
35 |
36 | export default InputColor;
37 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = 'Input';
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ))
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
28 |
29 | export { Popover, PopoverTrigger, PopoverContent }
30 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | ))
22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23 |
24 | const ScrollBar = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, orientation = "vertical", ...props }, ref) => (
28 |
41 |
42 |
43 | ))
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45 |
46 | export { ScrollArea, ScrollBar }
47 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as SliderPrimitive from '@radix-ui/react-slider';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
19 |
20 |
21 |
22 |
23 | ));
24 | Slider.displayName = SliderPrimitive.Root.displayName;
25 |
26 | export { Slider };
27 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
3 | import { type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | })
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 |
26 | {children}
27 |
28 |
29 | ))
30 |
31 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
32 |
33 | const ToggleGroupItem = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef &
36 | VariantProps
37 | >(({ className, children, variant, size, ...props }, ref) => {
38 | const context = React.useContext(ToggleGroupContext)
39 |
40 | return (
41 |
52 | {children}
53 |
54 | )
55 | })
56 |
57 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
58 |
59 | export { ToggleGroup, ToggleGroupItem }
60 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-10 px-3",
18 | sm: "h-9 px-2.5",
19 | lg: "h-11 px-5",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName
42 |
43 | export { Toggle, toggleVariants }
44 |
--------------------------------------------------------------------------------
/src/data.ts:
--------------------------------------------------------------------------------
1 | export const data = {
2 | itemIds: [
3 | "P_AwU6g66eSoV06DMrhry",
4 | "k03f-UuDsEGC4UBEtJqTl",
5 | "FcdiapyzrWDi5JLhyLBcw",
6 | "Np0-fXqyb4z9CWa6DBxmf",
7 | "on-wb329Ywfk77dL9V-Nb",
8 | "gDjKfK8JWDcDqKyTqIzFv",
9 | "4d5xO2FFwa2se64AdHthq",
10 | "93M2-3DNghMpdCnKWai6u",
11 | "l1eBqP8xJY-qtB-Y-svcJ",
12 | ],
13 | itemsMap: {
14 | P_AwU6g66eSoV06DMrhry: {
15 | id: "P_AwU6g66eSoV06DMrhry",
16 | name: "",
17 | type: "audio",
18 | display: {
19 | from: 0,
20 | to: 50503.39583333334,
21 | },
22 | trim: {
23 | from: 0,
24 | to: 50503.395833333336,
25 | },
26 | details: {
27 | src: "https://ik.imagekit.io/snapmotion/timer-voice.mp3",
28 | duration: 50503.395833333336,
29 | volume: 100,
30 | },
31 | metadata: {},
32 | isMain: false,
33 | },
34 | "Np0-fXqyb4z9CWa6DBxmf": {
35 | id: "Np0-fXqyb4z9CWa6DBxmf",
36 | type: "image",
37 | name: "",
38 | display: {
39 | from: 0,
40 | to: 5000,
41 | },
42 | details: {
43 | src: "https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562725/pexels-zhanzat-2922413_pdqbsh.jpg",
44 | preview: "",
45 | width: 5472,
46 | height: 3648,
47 | opacity: 100,
48 | transform: "scale(0.19736842105263158)",
49 | border: "none",
50 | borderRadius: "0",
51 | boxShadow: "none",
52 | top: "-1284px",
53 | left: "-2196px",
54 | },
55 | metadata: {},
56 | isMain: true,
57 | },
58 | FcdiapyzrWDi5JLhyLBcw: {
59 | id: "FcdiapyzrWDi5JLhyLBcw",
60 | type: "image",
61 | name: "",
62 | display: {
63 | from: 5000,
64 | to: 10000,
65 | },
66 | details: {
67 | src: "https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562718/pexels-eberhardgross-1366913_beh6zg.jpg",
68 | preview: "",
69 | width: 2432,
70 | height: 3648,
71 | opacity: 100,
72 | transform: "scale(0.29605263157894735)",
73 | border: "none",
74 | borderRadius: "0",
75 | boxShadow: "none",
76 | top: "-1284px",
77 | left: "-676px",
78 | },
79 | metadata: {},
80 | isMain: true,
81 | },
82 | "k03f-UuDsEGC4UBEtJqTl": {
83 | id: "k03f-UuDsEGC4UBEtJqTl",
84 | name: "",
85 | type: "video",
86 | display: {
87 | from: 10000,
88 | to: 31386.666999999998,
89 | },
90 | trim: {
91 | from: 0,
92 | to: 21386.666999999998,
93 | },
94 | details: {
95 | src: "https://res.cloudinary.com/drj5rmp5l/video/upload/v1722588970/153976-817104245_tiny_bg8fty.mp4",
96 | preview: "",
97 | width: 1280,
98 | height: 720,
99 | duration: 21386.666999999998,
100 | opacity: 100,
101 | volume: 100,
102 | transform: "scale(0.84375)",
103 | border: "none",
104 | borderRadius: "0",
105 | boxShadow: "none",
106 | top: "180px",
107 | left: "-100px",
108 | },
109 | metadata: {
110 | resourceId:
111 | "https://res.cloudinary.com/drj5rmp5l/video/upload/v1722588970/153976-817104245_tiny_bg8fty.mp4",
112 | },
113 | isMain: true,
114 | },
115 | "l1eBqP8xJY-qtB-Y-svcJ": {
116 | id: "l1eBqP8xJY-qtB-Y-svcJ",
117 | name: "",
118 | type: "text",
119 | display: {
120 | from: 0,
121 | to: 5000,
122 | },
123 | details: {
124 | fontFamily: "Roboto-Bold",
125 | fontSize: 120,
126 | fontWeight: "normal",
127 | fontStyle: "normal",
128 | textDecoration: "none",
129 | textAlign: "center",
130 | lineHeight: "normal",
131 | letterSpacing: "normal",
132 | wordSpacing: "normal",
133 | color: "#ffffff",
134 | backgroundColor: "transparent",
135 | border: "none",
136 | textShadow: "none",
137 | text: "Heading",
138 | opacity: 100,
139 | width: 600,
140 | wordWrap: "break-word",
141 | wordBreak: "break-all",
142 | WebkitTextStrokeColor: "#ffffff",
143 | WebkitTextStrokeWidth: "0px",
144 | top: "470px",
145 | left: "240px",
146 | height: 140,
147 | fontUrl:
148 | "https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf",
149 | },
150 | metadata: {},
151 | isMain: false,
152 | },
153 | "93M2-3DNghMpdCnKWai6u": {
154 | id: "93M2-3DNghMpdCnKWai6u",
155 | name: "",
156 | type: "text",
157 | display: {
158 | from: 5000,
159 | to: 10000,
160 | },
161 | details: {
162 | fontFamily: "Roboto-Bold",
163 | fontSize: 120,
164 | fontWeight: "normal",
165 | fontStyle: "normal",
166 | textDecoration: "none",
167 | textAlign: "center",
168 | lineHeight: "normal",
169 | letterSpacing: "normal",
170 | wordSpacing: "normal",
171 | color: "#ffffff",
172 | backgroundColor: "transparent",
173 | border: "none",
174 | textShadow: "none",
175 | text: "Heading",
176 | opacity: 100,
177 | width: 600,
178 | wordWrap: "break-word",
179 | wordBreak: "break-all",
180 | WebkitTextStrokeColor: "#ffffff",
181 | WebkitTextStrokeWidth: "0px",
182 | top: "470px",
183 | left: "240px",
184 | height: 140,
185 | fontUrl:
186 | "https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf",
187 | },
188 | metadata: {},
189 | isMain: false,
190 | },
191 | "4d5xO2FFwa2se64AdHthq": {
192 | id: "4d5xO2FFwa2se64AdHthq",
193 | name: "",
194 | type: "text",
195 | display: {
196 | from: 10000,
197 | to: 15000,
198 | },
199 | details: {
200 | fontFamily: "Roboto-Bold",
201 | fontSize: 120,
202 | fontWeight: "normal",
203 | fontStyle: "normal",
204 | textDecoration: "none",
205 | textAlign: "center",
206 | lineHeight: "normal",
207 | letterSpacing: "normal",
208 | wordSpacing: "normal",
209 | color: "#ffffff",
210 | backgroundColor: "transparent",
211 | border: "none",
212 | textShadow: "none",
213 | text: "Heading",
214 | opacity: 100,
215 | width: 600,
216 | wordWrap: "break-word",
217 | wordBreak: "break-all",
218 | WebkitTextStrokeColor: "#ffffff",
219 | WebkitTextStrokeWidth: "0px",
220 | top: "470px",
221 | left: "240px",
222 | height: 140,
223 | fontUrl:
224 | "https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf",
225 | },
226 | metadata: {},
227 | isMain: false,
228 | },
229 | gDjKfK8JWDcDqKyTqIzFv: {
230 | id: "gDjKfK8JWDcDqKyTqIzFv",
231 | name: "",
232 | type: "text",
233 | display: {
234 | from: 15000,
235 | to: 20000,
236 | },
237 | details: {
238 | fontFamily: "Roboto-Bold",
239 | fontSize: 120,
240 | fontWeight: "normal",
241 | fontStyle: "normal",
242 | textDecoration: "none",
243 | textAlign: "center",
244 | lineHeight: "normal",
245 | letterSpacing: "normal",
246 | wordSpacing: "normal",
247 | color: "#ffffff",
248 | backgroundColor: "transparent",
249 | border: "none",
250 | textShadow: "none",
251 | text: "Heading",
252 | opacity: 100,
253 | width: 600,
254 | wordWrap: "break-word",
255 | wordBreak: "break-all",
256 | WebkitTextStrokeColor: "#ffffff",
257 | WebkitTextStrokeWidth: "0px",
258 | top: "470px",
259 | left: "240px",
260 | height: 140,
261 | fontUrl:
262 | "https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf",
263 | },
264 | metadata: {},
265 | isMain: false,
266 | },
267 | "on-wb329Ywfk77dL9V-Nb": {
268 | id: "on-wb329Ywfk77dL9V-Nb",
269 | name: "",
270 | type: "text",
271 | display: {
272 | from: 20000,
273 | to: 25000,
274 | },
275 | details: {
276 | fontFamily: "Roboto-Bold",
277 | fontSize: 120,
278 | fontWeight: "normal",
279 | fontStyle: "normal",
280 | textDecoration: "none",
281 | textAlign: "center",
282 | lineHeight: "normal",
283 | letterSpacing: "normal",
284 | wordSpacing: "normal",
285 | color: "#ffffff",
286 | backgroundColor: "transparent",
287 | border: "none",
288 | textShadow: "none",
289 | text: "Heading",
290 | opacity: 100,
291 | width: 600,
292 | wordWrap: "break-word",
293 | wordBreak: "break-all",
294 | WebkitTextStrokeColor: "#ffffff",
295 | WebkitTextStrokeWidth: "0px",
296 | top: "470px",
297 | left: "240px",
298 | height: 140,
299 | fontUrl:
300 | "https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf",
301 | },
302 | metadata: {},
303 | isMain: false,
304 | },
305 | },
306 | transitionIds: [
307 | "Np0-fXqyb4z9CWa6DBxmf-FcdiapyzrWDi5JLhyLBcw",
308 | "FcdiapyzrWDi5JLhyLBcw-k03f-UuDsEGC4UBEtJqTl",
309 | ],
310 | transitionsMap: {
311 | "Np0-fXqyb4z9CWa6DBxmf-FcdiapyzrWDi5JLhyLBcw": {
312 | id: "Np0-fXqyb4z9CWa6DBxmf-FcdiapyzrWDi5JLhyLBcw",
313 | duration: 2000,
314 | fromId: "Np0-fXqyb4z9CWa6DBxmf",
315 | toId: "FcdiapyzrWDi5JLhyLBcw",
316 | type: "slide",
317 | trackId: "main",
318 | direction: "from-right",
319 | },
320 | "FcdiapyzrWDi5JLhyLBcw-k03f-UuDsEGC4UBEtJqTl": {
321 | id: "FcdiapyzrWDi5JLhyLBcw-k03f-UuDsEGC4UBEtJqTl",
322 | duration: 2000,
323 | fromId: "FcdiapyzrWDi5JLhyLBcw",
324 | toId: "k03f-UuDsEGC4UBEtJqTl",
325 | type: "none",
326 | trackId: "main",
327 | },
328 | },
329 | tracks: [
330 | {
331 | id: "2X_TBRFFOzdVvrPqFYPpN",
332 | items: [
333 | "l1eBqP8xJY-qtB-Y-svcJ",
334 | "93M2-3DNghMpdCnKWai6u",
335 | "4d5xO2FFwa2se64AdHthq",
336 | "gDjKfK8JWDcDqKyTqIzFv",
337 | "on-wb329Ywfk77dL9V-Nb",
338 | ],
339 | type: "text",
340 | accepts: ["text"],
341 | },
342 | {
343 | id: "main",
344 | type: "main",
345 | items: [
346 | "Np0-fXqyb4z9CWa6DBxmf",
347 | "FcdiapyzrWDi5JLhyLBcw",
348 | "k03f-UuDsEGC4UBEtJqTl",
349 | ],
350 | accepts: ["video", "image"],
351 | },
352 | {
353 | id: "7UjpbF9sz8CHMacL2Eoy0",
354 | items: ["P_AwU6g66eSoV06DMrhry"],
355 | type: "audio",
356 | accepts: ["audio", "video"],
357 | },
358 | ],
359 | size: {
360 | width: 1080,
361 | height: 1080,
362 | },
363 | duration: 50503.39583333334,
364 | fps: 30,
365 | projectId: "main",
366 | };
367 |
--------------------------------------------------------------------------------
/src/data/audio.ts:
--------------------------------------------------------------------------------
1 | export const AUDIOS = [
2 | {
3 | id: 1,
4 | name: 'Nature Walk',
5 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563682/nature-walk-124997_fs49zw.mp3',
6 | author: 'Olexy',
7 | },
8 | {
9 | id: 2,
10 | name: 'Nature Calls',
11 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563680/nature-calls-136344_wed2nh.mp3',
12 | author: 'Olexy',
13 | },
14 | {
15 | id: 3,
16 | name: 'Melody of Nature',
17 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563679/melody-of-nature-main-6672_vlp3yp.mp3',
18 | author: 'GoodBMusic',
19 | },
20 | {
21 | id: 4,
22 | name: 'Evolving Nature',
23 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563678/evolving-nature-221175_m9tr7k.mp3',
24 | author: 'MusicInMedia',
25 | },
26 | {
27 | id: 5,
28 | name: 'Deep Nature',
29 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563676/deep-nature-226130_z6adju.mp3',
30 | author: 'MusicInMedia',
31 | },
32 | {
33 | id: 6,
34 | name: 'Nature Documentary',
35 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563675/nature-documentary-171967_di7kcx.mp3',
36 | author: 'AlisiaBeats',
37 | },
38 | {
39 | id: 7,
40 | name: 'Nature Background',
41 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563674/nature-background-171966_dhefkp.mp3',
42 | author: 'AlisiaBeats',
43 | },
44 | {
45 | id: 8,
46 | name: 'Inspiring Nature',
47 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722563673/inspiring-nature-technology-11488_ehndvs.mp3',
48 | author: 'ComaMedia',
49 | },
50 | ];
51 |
--------------------------------------------------------------------------------
/src/data/images.ts:
--------------------------------------------------------------------------------
1 | export const IMAGES = [
2 | {
3 | id: 1,
4 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562726/pexels-asadphoto-1450361_h9mb2r.jpg',
5 | },
6 | {
7 | id: 2,
8 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562725/pexels-zhanzat-2922413_pdqbsh.jpg',
9 | },
10 | {
11 | id: 3,
12 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562725/pexels-kasuma-1785493_wpxzxa.jpg',
13 | },
14 | {
15 | id: 4,
16 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562724/pexels-rohan-dalal-62574732-8103080_lzwkii.jpg',
17 | },
18 | {
19 | id: 5,
20 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562722/pexels-qjpioneer-917510_byrne6.jpg',
21 | },
22 | {
23 | id: 6,
24 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562720/pexels-megha-lad-1686069-4248308_ajr7xt.jpg',
25 | },
26 | {
27 | id: 7,
28 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562719/pexels-nurseryart-370474_kweel1.jpg',
29 | },
30 | {
31 | id: 8,
32 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562718/pexels-eberhardgross-1366913_beh6zg.jpg',
33 | },
34 | {
35 | id: 9,
36 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562717/pexels-freestockpro-1007957_lcsbhh.jpg',
37 | },
38 | {
39 | id: 10,
40 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562716/pexels-bymalens-2268556_apww6i.jpg',
41 | },
42 | {
43 | id: 11,
44 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562716/pexels-alex-montes-892479-1820563_i9rcna.jpg',
45 | },
46 | {
47 | id: 12,
48 | src: 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722562715/pexels-alexazabache-3214944_r6uhbf.jpg',
49 | },
50 | ];
51 |
--------------------------------------------------------------------------------
/src/data/shapes.ts:
--------------------------------------------------------------------------------
1 | export const ICONS = [
2 | {
3 | id: 1,
4 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/square-sharp-solid_02csZc5Yo.svg",
5 | type: "StaticVector",
6 | },
7 | {
8 | id: 2,
9 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/circle-sharp-solid_ayCbPbs9Ft.svg",
10 | type: "StaticVector",
11 | },
12 | {
13 | id: 3,
14 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/triangle-sharp-solid_h2LbFsZ4e.svg",
15 | type: "StaticVector",
16 | },
17 | {
18 | id: 4,
19 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/ticket-simple-sharp-solid_hobsILqys.svg",
20 | type: "StaticVector",
21 | },
22 | {
23 | id: 5,
24 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/star-sharp-solid_39jcKyZbo.svg",
25 | type: "StaticVector",
26 | },
27 | {
28 | id: 6,
29 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/star-sharp-sharp-solid_tNERIa3BI.svg",
30 | type: "StaticVector",
31 | },
32 | {
33 | id: 7,
34 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/rectangle-wide-sharp-solid_3ObyIpAq4.svg",
35 | type: "StaticVector",
36 | },
37 | {
38 | id: 8,
39 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/rectangle-sharp-solid_hzr2CFW20.svg",
40 | type: "StaticVector",
41 | },
42 | {
43 | id: 9,
44 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/shield-sharp-solid_ihtnonUsl.svg",
45 | type: "StaticVector",
46 | },
47 | {
48 | id: 10,
49 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/rhombus-sharp-solid_9c6GXgNDC.svg",
50 | type: "StaticVector",
51 | },
52 | {
53 | id: 11,
54 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/seal-sharp-solid_USrZNoSRP.svg",
55 | type: "StaticVector",
56 | },
57 | {
58 | id: 12,
59 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/heart-sharp-solid_401LDhVm4.svg",
60 | type: "StaticVector",
61 | },
62 | {
63 | id: 13,
64 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/hexagon-sharp-solid_PQSxbGDj_.svg",
65 | type: "StaticVector",
66 | },
67 | {
68 | id: 14,
69 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/octagon-sharp-solid__VAncC4PtU.svg",
70 | type: "StaticVector",
71 | },
72 | {
73 | id: 15,
74 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/location-pin-sharp-solid_TEGM_IfPp.svg",
75 | type: "StaticVector",
76 | },
77 | {
78 | id: 16,
79 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/folder-sharp-solid_t_Spg-nDY0.svg",
80 | type: "StaticVector",
81 | },
82 | {
83 | id: 17,
84 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/calendar-sharp-solid_-PxQfYA7l.svg",
85 | type: "StaticVector",
86 | },
87 | {
88 | id: 18,
89 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/comment-sharp-solid_v8zfO6PT-K.svg",
90 | type: "StaticVector",
91 | },
92 | {
93 | id: 19,
94 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/diamond-sharp-solid_zq9tb9D6GX.svg",
95 | type: "StaticVector",
96 | },
97 | {
98 | id: 20,
99 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/bookmark-sharp-solid_iyCscs2D3.svg",
100 | type: "StaticVector",
101 | },
102 | {
103 | id: 21,
104 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/circle-half-stroke-sharp-solid_rOOQMQOa9x.svg",
105 | type: "StaticVector",
106 | },
107 | {
108 | id: 22,
109 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/crown-sharp-solid_0TuJ-ZAig.svg",
110 | type: "StaticVector",
111 | },
112 | {
113 | id: 23,
114 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/cloud-sharp-solid_-0Y7U-MWM.svg",
115 | type: "StaticVector",
116 | },
117 | {
118 | id: 24,
119 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/badge-sharp-solid_jT2oUDVR2.svg",
120 | type: "StaticVector",
121 | },
122 | {
123 | id: 25,
124 | src: "https://ik.imagekit.io/uonadbo34e6/sharp-solid/certificate-sharp-solid_Rc6oTP3dz.svg",
125 | type: "StaticVector",
126 | },
127 | ];
128 |
--------------------------------------------------------------------------------
/src/data/uploads.ts:
--------------------------------------------------------------------------------
1 | export const UPLOADS = [
2 | {
3 | id: '1',
4 | src: 'https://ik.imagekit.io/snapmotion/upload-video-1.mp4',
5 | type: 'video',
6 | },
7 | {
8 | id: '2',
9 | src: 'https://ik.imagekit.io/snapmotion/upload-video-2.mp4',
10 | type: 'video',
11 | },
12 | {
13 | id: '3',
14 | src: 'https://ik.imagekit.io/snapmotion/upload-video-3.mp4',
15 | type: 'video',
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/src/data/video.ts:
--------------------------------------------------------------------------------
1 | export const VIDEOS = [
2 | {
3 | id: 1,
4 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722562801/11599511-uhd_3840_2160_25fps_u9lfct.mp4',
5 | preview:
6 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722587890/11599511-uhd_3840_2160_25fps_rprjzo.png',
7 | },
8 | {
9 | id: 3,
10 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722562794/4889113-uhd_2160_4096_24fps_khn4zw.mp4',
11 | preview:
12 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722587892/4889113-uhd_2160_4096_24fps_usq001.png',
13 | },
14 | {
15 | id: 4,
16 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722562778/8438341-hd_1080_1920_30fps_a5r8xr.mp4',
17 | preview:
18 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722587889/8438341-hd_1080_1920_30fps_mmsk8s.png',
19 | },
20 | {
21 | id: 6,
22 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722562776/854136-hd_1920_1080_25fps_vujdye.mp4',
23 | preview:
24 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722587889/854136-hd_1920_1080_25fps_kawpp9.png',
25 | },
26 | {
27 | id: 7,
28 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722562773/857195-hd_1280_720_25fps_pvpdcr.mp4',
29 | preview:
30 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722587889/857195-hd_1280_720_25fps_te1ljk.png',
31 | },
32 | {
33 | id: 8,
34 | preview:
35 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722588897/223551_tiny_tns9ok.png',
36 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722588971/223551_tiny_kkejuq.mp4',
37 | },
38 | {
39 | id: 9,
40 | preview:
41 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722588896/153976-817104245_tiny_e5hgts.png',
42 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722588970/153976-817104245_tiny_bg8fty.mp4',
43 | },
44 | {
45 | id: 10,
46 | preview:
47 | 'https://res.cloudinary.com/drj5rmp5l/image/upload/v1722588896/111204-689949818_small_ddtaon.png',
48 | src: 'https://res.cloudinary.com/drj5rmp5l/video/upload/v1722588970/111204-689949818_small_ipljbx.mp4',
49 | },
50 | ];
51 |
--------------------------------------------------------------------------------
/src/global/dispatcher.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from "rxjs";
2 | import {
3 | Dispatcher,
4 | EventBusData,
5 | DispatcherReturnType,
6 | } from "../interfaces/rxjs";
7 |
8 | function createDispatcher(): DispatcherReturnType {
9 | const bus = new Subject();
10 | const dispatch: Dispatcher = (key, val) => bus.next({ key, val });
11 | return { bus, dispatch };
12 | }
13 |
14 | export const dispatcher = createDispatcher();
15 |
16 | export { filter } from "rxjs";
17 |
--------------------------------------------------------------------------------
/src/global/events.ts:
--------------------------------------------------------------------------------
1 | // Artboard events
2 | export const ARTBOARD_PREFIX = "artboard";
3 | export const ARTBOARD_STATE = `${ARTBOARD_PREFIX}:state`;
4 | export const ARTBOARD_SELECTION = `${ARTBOARD_PREFIX}:selection`;
5 | export const ARTBOARD_IS_LOADING = `${ARTBOARD_PREFIX}:isLoading`;
6 |
7 | export const ADD_SUFFIX = "add";
8 |
9 | export const TRACK_PREFIX = "track";
10 | export const TRACK_ITEMS_PREFIX = "items";
11 | export const TRACKS_CHANGED = `${TRACK_PREFIX}:changed`;
12 | export const TRACK_ITEMS_CHANGED = `${TRACK_ITEMS_PREFIX}:changed`;
13 |
14 | export const STATE_PREFIX = "state";
15 | export const STATE_CHANGED = `${STATE_PREFIX}:changed`;
16 | // Add new element events
17 | export const ADD_PREFIX = "add";
18 | export const ADD_TEXT = `${ADD_PREFIX}:text`;
19 | export const ADD_VIDEO = `${ADD_PREFIX}:video`;
20 | export const ADD_AUDIO = `${ADD_PREFIX}:audio`;
21 | export const ADD_MAGIC_VIDEO = `${ADD_PREFIX}:magic:video`;
22 | export const ADD_PLACEHOLDER = `${ADD_PREFIX}:placeholder`;
23 | export const ADD_IMAGE = `${ADD_PREFIX}:image`;
24 | export const ADD_ILLUSTRATION = `${ADD_PREFIX}:illustration`;
25 | export const ADD_BASIC_SHAPE = `${ADD_PREFIX}:basicShape`;
26 | export const ADD_TEXTURE = `${ADD_PREFIX}:texture`;
27 | export const ADD_TEMPLATE = `${ADD_PREFIX}:template`;
28 | export const ADD_DESIGN = `${ADD_PREFIX}:design`;
29 | export const ADD_PHOTO = `${ADD_PREFIX}:photo`;
30 | export const ADD_MASK = `${ADD_PREFIX}:mask`;
31 | export const ADD_MOCKUP_TEMPLATE = `${ADD_PREFIX}:mockupTemplate`;
32 | export const ADD_TRANSITION = `${ADD_PREFIX}:transition`;
33 | export const ADD_ANIMATION = `${ADD_PREFIX}:animation`;
34 |
35 | // Edit artboard elements
36 | export const EDIT_PREFIX = "edit";
37 | export const EDIT_OBJECT = `${EDIT_PREFIX}:object`;
38 | export const EDIT_TEXT = `${EDIT_PREFIX}:text`;
39 | export const EDIT_SHAPE = `${EDIT_PREFIX}:shape`;
40 | export const EDIT_TEXTURE = `${EDIT_PREFIX}:texture`;
41 | export const EDIT_BACKGROUND_IMAGE = `${EDIT_PREFIX}:backgroundImage`;
42 | export const EDIT_ANIMATION = `${EDIT_PREFIX}:animation`;
43 |
44 | export const ENTER_EDIT_MODE = `enterEditMode`;
45 |
46 | export const PLAYER_PREFIX = "player";
47 | export const PLAYER_PLAY = `${PLAYER_PREFIX}:play`;
48 | export const PLAYER_PAUSE = `${PLAYER_PREFIX}:pause`;
49 | export const PLAYER_SEEK = `${PLAYER_PREFIX}:seek`;
50 | export const PLAYER_SEEK_TO = `${PLAYER_PREFIX}:seekTo`;
51 | export const PLAYER_SEEK_BY = `${PLAYER_PREFIX}:seekBy`;
52 | export const PLAYER_TOGGLE_PLAY = `${PLAYER_PREFIX}:togglePlay`;
53 |
54 | // Active object events
55 | export const ACTIVE_PREFIX = "active";
56 | export const ACTIVE_SET = `${ACTIVE_PREFIX}:set`;
57 | export const STYLE_TAG = "style"; //Events that change styles should have this property
58 | export const ACTIVE_DELETE = `${ACTIVE_PREFIX}:delete`;
59 | export const ACTIVE_OPACITY = `${ACTIVE_PREFIX}:opacity`;
60 | export const ACTIVE_MOVE = `${ACTIVE_PREFIX}:move`;
61 | export const ACTIVE_MOVE_BY = `${ACTIVE_PREFIX}:moveBy`;
62 | export const ACTIVE_MOVED = `${ACTIVE_PREFIX}:moved`;
63 | export const ACTIVE_COPY = `${ACTIVE_PREFIX}:copy`;
64 | export const ACTIVE_PASTE = `${ACTIVE_PREFIX}:paste`;
65 | export const ACTIVE_CLONE = `${ACTIVE_PREFIX}:clone`;
66 | export const ACTIVE_SPLIT = `${ACTIVE_PREFIX}:split`;
67 | export const ACTIVE_CUT = `${ACTIVE_PREFIX}:cut`;
68 | export const ACTIVE_REFLECT_HORIZONTAL = `${ACTIVE_PREFIX}:reflectHorizontal`;
69 | export const ACTIVE_REFLECT_VERTICAL = `${ACTIVE_PREFIX}:reflectVertical`;
70 | export const ACTIVE_GROUP = `${ACTIVE_PREFIX}:group`;
71 | export const ACTIVE_LOCK = `${ACTIVE_PREFIX}:lock`;
72 | export const ACTIVE_ALIGN = `${ACTIVE_PREFIX}:align`;
73 | export const ACTIVE_GLYPH = `${ACTIVE_PREFIX}:glyph`;
74 | export const ACTIVE_OVERLAY_MODE = `${ACTIVE_PREFIX}:overlayMode`;
75 | export const ACTIVE_COLOR = `${ACTIVE_PREFIX}:${STYLE_TAG}:color`;
76 | export const ACTIVE_STROKE_WIDTH = `${ACTIVE_PREFIX}:${STYLE_TAG}:strokeWidth`;
77 | export const ACTIVE_FONTSIZE = `${ACTIVE_PREFIX}:${STYLE_TAG}:fontSize`;
78 | export const ACTIVE_LETTERSPACING = `${ACTIVE_PREFIX}:${STYLE_TAG}:letterSpacing`;
79 | export const ACTIVE_FONTFAMILY = `${ACTIVE_PREFIX}:${STYLE_TAG}:fontFamily`;
80 | export const ACTIVE_TEXTALIGNMENT = `${ACTIVE_PREFIX}:${STYLE_TAG}:textAlignment`;
81 | export const ACTIVE_UPPERCASE = `${ACTIVE_PREFIX}:${STYLE_TAG}:uppercase`;
82 | export const ACTIVE_UNDERLINE = `${ACTIVE_PREFIX}:${STYLE_TAG}:underline`;
83 | export const ACTIVE_LIGATURES = `${ACTIVE_PREFIX}:${STYLE_TAG}:ligatures`;
84 | export const ACTIVE_LINEHEIGHT = `${ACTIVE_PREFIX}:${STYLE_TAG}:lineHeight`;
85 | export const ACTIVE_VARIATION = `${ACTIVE_PREFIX}:${STYLE_TAG}:variation`;
86 | export const ACTIVE_SHADOW = `${ACTIVE_PREFIX}:${STYLE_TAG}:shadow`;
87 | export const ACTIVE_TRANSFORM = `${ACTIVE_PREFIX}:${STYLE_TAG}:transform`;
88 | export const ACTIVE_TRANSFORM_CURVE = `${ACTIVE_PREFIX}:${STYLE_TAG}:transformCurve`;
89 | export const ACTIVE_DECORATION = `${ACTIVE_PREFIX}:${STYLE_TAG}:decoration`;
90 | export const ACTIVE_CLIPPING_MASK = `${ACTIVE_PREFIX}:clippingMask`;
91 | export const ACTIVE_EDIT_MODE = `${ACTIVE_PREFIX}:editMode`;
92 | export const ACTIVE_TIDY = `${ACTIVE_PREFIX}:tidy`;
93 | export const ACTIVE_REMOVE_BG = `${ACTIVE_PREFIX}:removeBackground`;
94 | export const ACTIVE_VECTORIZE = `${ACTIVE_PREFIX}:vectorize`;
95 | export const ACTIVE_FILTER = `${ACTIVE_PREFIX}:filter`;
96 | export const ACTIVE_FILTER_RESET = `${ACTIVE_PREFIX}:filterReset`;
97 | export const ACTIVE_COLOR_PREVIEW = `${ACTIVE_PREFIX}:previewColor`;
98 | export const ACTIVE_STOP_PREVIEW_COLOR = `${ACTIVE_PREFIX}:stopPreviewColor`;
99 | export const ACTIVE_AS_BACKGROUND_IMAGE = `${ACTIVE_PREFIX}:asBackgroundImage`;
100 |
101 | // Layer events
102 | export const LAYER_PREFIX = "layer";
103 | export const LAYER_LOCKED = `${LAYER_PREFIX}:locked`;
104 | export const LAYER_HIDDEN = `${LAYER_PREFIX}:hidden`;
105 | export const LAYER_MOVE = `${LAYER_PREFIX}:move`;
106 | export const LAYER_SELECT = `${LAYER_PREFIX}:select`;
107 | export const LAYER_SELECTION = `${LAYER_PREFIX}:selection`;
108 | export const LAYER_SEND_TO = `${LAYER_PREFIX}:sendTo`;
109 | export const LAYER_RENAME = `${LAYER_PREFIX}:rename`;
110 | export const LAYER_EDITING_NAME = `${LAYER_PREFIX}:editingName`;
111 |
112 | export const LAYER_COPY = `${LAYER_PREFIX}:copy`;
113 | export const LAYER_PASTE = `${LAYER_PREFIX}:paste`;
114 | export const LAYER_CLONE = `${LAYER_PREFIX}:clone`;
115 | export const LAYER_SPLIT = `${LAYER_PREFIX}:split`;
116 | export const LAYER_CUT = `${LAYER_PREFIX}:cut`;
117 | export const LAYER_DELETE = `${LAYER_PREFIX}:delete`;
118 |
119 | // History events
120 | export const HISTORY_PREFIX = "history";
121 | export const HISTORY_UNDO = `${HISTORY_PREFIX}:undo`;
122 | export const HISTORY_REDO = `${HISTORY_PREFIX}:redo`;
123 | export const HISTORY_RESET = `${HISTORY_PREFIX}:reset`;
124 |
125 | // Design events
126 | export const DESIGN_PREFIX = "design";
127 | export const DESIGN_LOAD = `${DESIGN_PREFIX}:load`;
128 | export const DESIGN_RENAME = `${DESIGN_PREFIX}:rename`;
129 | export const DESIGN_SAVE = `${DESIGN_PREFIX}:save`;
130 | export const DESIGN_NEW = `${DESIGN_PREFIX}:newDesign`;
131 | export const DESIGN_DELETE = `${DESIGN_PREFIX}:deleteDesign`;
132 | export const DESIGN_DUPLICATE = `${DESIGN_PREFIX}:duplicateDesign`;
133 | export const DESIGN_SHARE = `${DESIGN_PREFIX}:share`;
134 | export const DESIGN_RESIZE = `${DESIGN_PREFIX}:resize`;
135 | // drag events
136 | export const DRAG_PREFIX = "drag";
137 | export const DRAG_START = `${DRAG_PREFIX}:start`;
138 | export const DRAG_END = `${DRAG_PREFIX}:end`;
139 |
140 | export const EDITOR_PREFIX = "editor";
141 |
142 | export const SELECTION_PREFIX = "selection";
143 | export const SELECTION_UPDATED = `${SELECTION_PREFIX}:updated`;
144 |
--------------------------------------------------------------------------------
/src/global/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./dispatcher";
2 | export * from "./events";
3 |
--------------------------------------------------------------------------------
/src/globals.css:
--------------------------------------------------------------------------------
1 | /* @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap'); */
2 | @import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,100;9..40,200;9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800;9..40,900&display=swap');
3 | @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
4 | @tailwind base;
5 | @tailwind components;
6 | @tailwind utilities;
7 |
8 | @layer base {
9 | :root {
10 | --background: 0 0% 100%;
11 | --foreground: 240 10% 3.9%;
12 | --card: 0 0% 100%;
13 | --card-foreground: 240 10% 3.9%;
14 | --popover: 0 0% 100%;
15 | --popover-foreground: 240 10% 3.9%;
16 | --primary: 240 5.9% 10%;
17 | --primary-foreground: 0 0% 98%;
18 | --secondary: 240 4.8% 95.9%;
19 | --secondary-foreground: 240 5.9% 10%;
20 | --muted: 240 4.8% 95.9%;
21 | --muted-foreground: 240 3.8% 46.1%;
22 | --accent: 240 4.8% 95.9%;
23 | --accent-foreground: 240 5.9% 10%;
24 | --destructive: 0 84.2% 60.2%;
25 | --destructive-foreground: 0 0% 98%;
26 | --border: 240 5.9% 90%;
27 | --input: 240 5.9% 90%;
28 | --ring: 240 5.9% 10%;
29 | --radius: 0.5rem;
30 | }
31 |
32 | .dark {
33 | --background: 240 10% 3.9%;
34 | --foreground: 0 0% 98%;
35 | --card: 240 10% 3.9%;
36 | --card-foreground: 0 0% 98%;
37 | --popover: 240 10% 3.9%;
38 | --popover-foreground: 0 0% 98%;
39 | --primary: 0 0% 98%;
40 | --primary-foreground: 240 5.9% 10%;
41 | --secondary: 240 3.7% 15.9%;
42 | --secondary-foreground: 0 0% 98%;
43 | --muted: 240 3.7% 15.9%;
44 | --muted-foreground: 240 5% 64.9%;
45 | --accent: 240 3.7% 15.9%;
46 | --accent-foreground: 0 0% 98%;
47 | --destructive: 0 62.8% 30.6%;
48 | --destructive-foreground: 0 0% 98%;
49 | --border: 240 3.7% 15.9%;
50 | --input: 240 3.7% 15.9%;
51 | --ring: 240 4.9% 83.9%;
52 |
53 | --background-1: theme('colors.grey.1300');
54 | --background-2: theme('colors.grey.1200');
55 | --background-3: theme('colors.grey.1100');
56 | --background-4: theme('colors.grey.1000');
57 | --background-5: theme('colors.grey.950');
58 | --background-6: theme('colors.grey.900');
59 | --background-7: theme('colors.grey.800');
60 | --background-8: theme('colors.grey.700');
61 | --background-9: theme('colors.grey.600');
62 |
63 | --text-primary: #fafbff;
64 | --text-secondary: rgba(246, 247, 255, 0.7);
65 | --text-tertiary: rgba(237, 240, 253, 0.5);
66 | --text-placeholder: rgba(229, 233, 250, 0.4);
67 | --text-disable: rgba(222, 227, 247, 0.2);
68 | --text-inverted: #090c14;
69 | --text-content-primary: #fff;
70 | --text-content-secondary: rgba(194, 207, 214, 0.8);
71 | --text-content-tertiary: rgba(189, 209, 219, 0.5);
72 | --text-content-placeholder: rgba(184, 211, 224, 0.4);
73 | --text-content-disable: rgba(178, 213, 230, 0.25);
74 | --fill-bg-1: #0e0e11;
75 | --fill-bg-2: #1c1d21;
76 | --fill-bg-3: #25262b;
77 | --fill-bg-4: #2f3036;
78 | --bg-5: #43434c;
79 | --bg-6: #f3f3f3;
80 | --fill-transparency-block: rgba(229, 236, 255, 0.1);
81 | --fill-transparency-hover: rgba(229, 236, 255, 0.14);
82 | --fill-transparency-pressed: rgba(229, 236, 255, 0.18);
83 | --scenes-toast: #27272f;
84 | --scenes-overlay: #2f3036;
85 | --scenes-panel: rgba(18, 18, 20, 0.8);
86 | --scenes-guide: rgba(255, 227, 87, 0.92);
87 | --line-1: hsla(0, 0%, 100%, 0.16);
88 | --line-2: hsla(0, 0%, 100%, 0.12);
89 | --line-3: hsla(0, 0%, 100%, 0.06);
90 | --shadow-008: hsla(0, 0%, 100%, 0.08);
91 | --shadow-016: hsla(0, 0%, 100%, 0.16);
92 | --shadow-024: hsla(0, 0%, 100%, 0.24);
93 | --shadow-032: hsla(0, 0%, 100%, 0.32);
94 | --white-inverted: #000;
95 | --black-inverted: #fff;
96 | }
97 | }
98 |
99 | @layer base {
100 | * {
101 | @apply border-border;
102 | }
103 | body {
104 | @apply bg-background text-foreground;
105 | }
106 | }
107 |
108 | @layer utilities {
109 | @variants responsive {
110 | .masonry {
111 | column-gap: 1.5em;
112 | column-count: 1;
113 | }
114 | .masonry-sm {
115 | gap: 0.5rem;
116 | column-count: 2;
117 | }
118 | }
119 | }
120 |
121 | .dm-sans-bold {
122 | font-family: 'DM Sans', sans-serif;
123 | font-optical-sizing: auto;
124 | font-weight: 500;
125 | font-style: normal;
126 | }
127 |
128 | /* --lvv-text-primary: #fafbff;
129 | --lvv-text-secondary: rgba(246,247,255,.7);
130 | --lvv-text-tertiary: rgba(237,240,253,.5);
131 | --lvv-text-placeholder: rgba(229,233,250,.4);
132 | --lvv-text-disable: rgba(222,227,247,.2);
133 | --lvv-text-inverted: #090c14;
134 | --lvv-text-content-primary: #fff;
135 | --lvv-text-content-secondary: rgba(194,207,214,.8);
136 | --lvv-text-content-tertiary: rgba(189,209,219,.5);
137 | --lvv-text-content-placeholder: rgba(184,211,224,.4);
138 | --lvv-text-content-disable: rgba(178,213,230,.25);
139 | --lvv-fill-bg-1: #0e0e11;
140 | --lvv-fill-bg-2: #1c1d21;
141 | --lvv-fill-bg-3: #25262b;
142 | --lvv-fill-bg-4: #2f3036;
143 | --lvv-bg-5: #43434c;
144 | --lvv-bg-6: #f3f3f3;
145 | --lvv-fill-transparency-block: rgba(229,236,255,.1);
146 | --lvv-fill-transparency-hover: rgba(229,236,255,.14);
147 | --lvv-fill-transparency-pressed: rgba(229,236,255,.18);
148 | --lvv-scenes-toast: #27272f;
149 | --lvv-scenes-overlay: #2f3036;
150 | --lvv-scenes-panel: rgba(18,18,20,.8);
151 | --lvv-scenes-guide: rgba(255,227,87,.92);
152 | --lvv-line-1: hsla(0,0%,100%,.16);
153 | --lvv-line-2: hsla(0,0%,100%,.12);
154 | --lvv-line-3: hsla(0,0%,100%,.06);
155 | --lvv-shadow-008: hsla(0,0%,100%,.08);
156 | --lvv-shadow-016: hsla(0,0%,100%,.16);
157 | --lvv-shadow-024: hsla(0,0%,100%,.24);
158 | --lvv-shadow-032: hsla(0,0%,100%,.32);
159 | --lvv-white-inverted: #000;
160 | --lvv-black-inverted: #fff; */
161 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | display: flex;
4 | place-items: center;
5 | min-width: 320px;
6 | min-height: 100vh;
7 | }
8 |
--------------------------------------------------------------------------------
/src/interfaces/editor.ts:
--------------------------------------------------------------------------------
1 | export interface IFont {
2 | id: string;
3 | family: string;
4 | fullName: string;
5 | postScriptName: string;
6 | preview: string;
7 | style: string;
8 | url: string;
9 | category: string;
10 | createdAt: string;
11 | updatedAt: string;
12 | userId: string | null;
13 | }
14 |
15 | export interface ICompactFont {
16 | family: string;
17 | styles: IFont[];
18 | default: IFont;
19 | name?: string;
20 | }
21 |
22 | export interface IDataState {
23 | fonts: IFont[];
24 | compactFonts: ICompactFont[];
25 | setFonts: (fonts: IFont[]) => void;
26 | setCompactFonts: (compactFonts: ICompactFont[]) => void;
27 | }
28 |
29 | export type IPropertyType = "textContent" | "fontSize" | "color";
30 |
31 | export type ItemType = "textbox" | "element" | "image";
32 |
33 | export interface IPosition {
34 | top: number;
35 | left: number;
36 | transform: string;
37 | }
38 |
39 | export interface ILayerBase {
40 | id: string;
41 | name: string;
42 | type: ItemType;
43 | preview?: string;
44 | position?: IPosition | null;
45 | details: Record;
46 | metadata: Record;
47 | isMain?: boolean;
48 | animation?: any;
49 | }
50 |
51 | export interface IText extends ILayerBase {
52 | type: ItemType;
53 | details: {
54 | text: string;
55 | fontSize: number;
56 | fontFamily: string;
57 | fontUrl: string;
58 | color: string;
59 | align: string;
60 | lineHeight: number;
61 | letterSpacing: number;
62 | };
63 | }
64 |
65 | export interface IImage extends ILayerBase {
66 | type: "image";
67 | details: {
68 | src: string;
69 | width: number;
70 | height: number;
71 | background?: string;
72 | };
73 | }
74 |
75 | export interface IMetadata {
76 | resourceId: string;
77 | order: number;
78 | }
79 |
80 | type TextAlign = "left" | "right" | "center" | "justify";
81 | type FontWeight = "normal" | "bold" | "bolder" | "lighter" | number; // number for values like 100, 200, etc.
82 | type TextDecoration = "none" | "underline" | "overline" | "line-through";
83 | type TextTransform = "none" | "capitalize" | "uppercase" | "lowercase";
84 | type FontStyle = "normal" | "italic" | "oblique";
85 | type Display = "block" | "inline" | "inline-block" | "flex" | "grid";
86 | type Position = "static" | "relative" | "absolute" | "fixed" | "sticky";
87 |
88 | interface ICommonDetails {
89 | width?: number;
90 | height?: number;
91 | transform?: string;
92 | opacity?: number;
93 | border?: string;
94 | borderRadius?: string;
95 | boxShadow?: string;
96 | top?: number | string;
97 | left?: number | string;
98 | }
99 |
100 | interface ITextDetails extends ICommonDetails {
101 | text?: string;
102 | fontSize?: number;
103 | fontFamily?: string;
104 | fontUrl?: string;
105 | color?: string;
106 | align?: string;
107 | textAlign?: TextAlign;
108 | lineHeight?: number;
109 | letterSpacing?: number;
110 | textDecoration?: TextDecoration;
111 | fontWeight?: FontWeight;
112 | wordSpacing?: number;
113 | textShadow?: string;
114 | backgroundColor?: string;
115 | }
116 |
117 | interface IImageDetails extends ICommonDetails {
118 | src: string;
119 | }
120 |
121 | export type ILayer =
122 | | (ILayerBase & { type: "textbox"; details: ITextDetails })
123 | | (ILayerBase & { type: "image"; details: IImageDetails });
124 |
125 | export interface ILayersMap {
126 | [id: string]: ILayer;
127 | }
128 |
129 | export interface IDesign {
130 | id: string | number;
131 | size: {
132 | width: number;
133 | height: number;
134 | type?: string; // landscape | portrait | square | custom
135 | };
136 | duration: number; // in miliseconds
137 | itemIds: string[];
138 | itemsMap: ILayersMap;
139 | }
140 |
--------------------------------------------------------------------------------
/src/interfaces/layout.ts:
--------------------------------------------------------------------------------
1 | export type IMenuItem =
2 | | 'uploads'
3 | | 'templates'
4 | | 'videos'
5 | | 'images'
6 | | 'shapes'
7 | | 'audios'
8 | | 'transitions'
9 | | 'texts';
10 | export interface ILayoutState {
11 | activeMenuItem: IMenuItem | null;
12 | showMenuItem: boolean;
13 | showControlItem: boolean;
14 | showToolboxItem: boolean;
15 | activeToolboxItem: string | null;
16 | setActiveMenuItem: (showMenu: IMenuItem | null) => void;
17 | setShowMenuItem: (showMenuItem: boolean) => void;
18 | setShowControlItem: (showControlItem: boolean) => void;
19 | setShowToolboxItem: (showToolboxItem: boolean) => void;
20 | setActiveToolboxItem: (activeToolboxItem: string | null) => void;
21 | }
22 |
--------------------------------------------------------------------------------
/src/interfaces/rxjs.ts:
--------------------------------------------------------------------------------
1 | import { Subject } from 'rxjs';
2 |
3 | export type EventBusData = {
4 | key: string;
5 | val?: {
6 | payload: any;
7 | options?: any;
8 | };
9 | };
10 |
11 | export type Dispatcher = (
12 | key: string,
13 | val?: {
14 | payload: any;
15 | options?: any;
16 | },
17 | ) => void;
18 |
19 | export interface DispatcherReturnType {
20 | bus: Subject;
21 | dispatch: Dispatcher;
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.tsx";
4 | import { ThemeProvider } from "@/components/theme-provider";
5 |
6 | import "./globals.css";
7 |
8 | ReactDOM.createRoot(document.getElementById("root")!).render(
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/store/use-data-state.ts:
--------------------------------------------------------------------------------
1 | import { IDataState } from '@/interfaces/editor';
2 | import { create } from 'zustand';
3 |
4 | const useDataState = create((set) => ({
5 | fonts: [],
6 | compactFonts: [],
7 | setFonts: (fonts) => set({ fonts }),
8 | setCompactFonts: (compactFonts) => set({ compactFonts }),
9 | }));
10 |
11 | export default useDataState;
12 |
--------------------------------------------------------------------------------
/src/store/use-layout-store.ts:
--------------------------------------------------------------------------------
1 | import { ILayoutState } from '@/interfaces/layout';
2 | import { create } from 'zustand';
3 |
4 | const useLayoutStore = create((set) => ({
5 | activeMenuItem: null,
6 | showMenuItem: false,
7 | showControlItem: false,
8 | showToolboxItem: false,
9 | activeToolboxItem: null,
10 | setActiveMenuItem: (showMenu) => set({ activeMenuItem: showMenu }),
11 | setShowMenuItem: (showMenuItem) => set({ showMenuItem }),
12 | setShowControlItem: (showControlItem) => set({ showControlItem }),
13 | setShowToolboxItem: (showToolboxItem) => set({ showToolboxItem }),
14 | setActiveToolboxItem: (activeToolboxItem) => set({ activeToolboxItem }),
15 | }));
16 |
17 | export default useLayoutStore;
18 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Canvas as BaseCanvas,
3 | FabricObject as BaseFabricObject,
4 | CanvasEvents as BaseCanvasEvents,
5 | } from "fabric";
6 |
7 | declare module "fabric" {
8 | export declare class Canvas extends BaseCanvas {
9 | __eventListeners: {};
10 | positionBeforeTransform?: {
11 | top: number;
12 | left: number;
13 | };
14 | }
15 |
16 | export declare class FabricObject extends BaseFabricObject {
17 | id: string;
18 | isAlignmentAuxiliary?: boolean;
19 | setSelected(selected: boolean): void;
20 | updateCoords(size?: number): void;
21 | timelineScale: number;
22 | accepts: ItemType[];
23 | itemType: string;
24 | items: string[];
25 | text: string;
26 | fontUrl: string;
27 | isMain?: boolean;
28 | display?: {
29 | from: number;
30 | to: number;
31 | };
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/utils/fonts.ts:
--------------------------------------------------------------------------------
1 | import { ICompactFont, IFont } from '@/interfaces/editor';
2 | import { groupBy } from 'lodash';
3 |
4 | export const loadFonts = (fonts: { name: string; url: string }[]) => {
5 | const promisesList = fonts.map((font) => {
6 | return new FontFace(font.name, `url(${font.url})`)
7 | .load()
8 | .catch((err) => err);
9 | });
10 | return new Promise((resolve, reject) => {
11 | Promise.all(promisesList)
12 | .then((res) => {
13 | res.forEach((uniqueFont) => {
14 | if (uniqueFont && uniqueFont.family) {
15 | document.fonts.add(uniqueFont);
16 | resolve(true);
17 | }
18 | });
19 | })
20 | .catch((err) => reject(err));
21 | });
22 | };
23 |
24 | const findDefaultFont = (fonts: IFont[]): IFont => {
25 | const regularFont = fonts.find((font) =>
26 | font.fullName.toLowerCase().includes('regular'),
27 | );
28 |
29 | return regularFont ? regularFont : fonts[0];
30 | };
31 |
32 | export const getCompactFontData = (fonts: IFont[]): ICompactFont[] => {
33 | const compactFontsMap: { [key: string]: ICompactFont } = {};
34 | // lodash groupby
35 | const fontsGroupedByFamily = groupBy(fonts, (font) => font.family);
36 |
37 | Object.keys(fontsGroupedByFamily).forEach((family) => {
38 | const fontsInFamily = fontsGroupedByFamily[family];
39 | const defaultFont = findDefaultFont(fontsInFamily);
40 | const compactFont: ICompactFont = {
41 | family: family,
42 | styles: fontsInFamily,
43 | default: defaultFont,
44 | };
45 | compactFontsMap[family] = compactFont;
46 | });
47 |
48 | return Object.values(compactFontsMap);
49 | };
50 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme');
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ['class'],
6 | content: [
7 | './pages/**/*.{ts,tsx}',
8 | './components/**/*.{ts,tsx}',
9 | './app/**/*.{ts,tsx}',
10 | './src/**/*.{ts,tsx}',
11 | ],
12 | prefix: '',
13 | theme: {
14 | fontFamily: {
15 | sans: ['DM Sans', ...defaultTheme.fontFamily.sans],
16 | },
17 | container: {
18 | center: true,
19 | padding: '2rem',
20 | screens: {
21 | '2xl': '1400px',
22 | },
23 | },
24 | extend: {
25 | colors: {
26 | border: 'hsl(var(--border))',
27 | input: 'hsl(var(--input))',
28 | ring: 'hsl(var(--ring))',
29 | background: 'hsl(var(--background))',
30 | 'text-primary': 'var(--text-primary)',
31 | 'text-secondary': 'var(--text-secondary)',
32 | 'text-tertiary': 'var(--text-tertiary)',
33 | 'text-placeholder': 'var(--text-placeholder)',
34 | 'text-disable': 'var(--text-disable)',
35 | 'text-inverted': 'var(--text-inverted)',
36 | 'text-content-primary': 'var(--text-content-primary)',
37 | 'text-content-secondary': 'var(--text-content-secondary)',
38 | 'text-content-tertiary': 'var(--text-content-tertiary)',
39 | 'text-content-placeholder': 'var(--text-content-placeholder)',
40 | 'text-content-disable': 'var(--text-content-disable)',
41 | 'fill-bg-1': 'var(--fill-bg-1)',
42 | 'fill-bg-2': 'var(--fill-bg-2)',
43 | 'fill-bg-3': 'var(--fill-bg-3)',
44 | 'fill-bg-4': 'var(--fill-bg-4)',
45 | 'bg-5': 'var(--bg-5)',
46 | 'bg-6': 'var(--bg-6)',
47 | 'fill-transparency-block': 'var(--fill-transparency-block)',
48 | 'fill-transparency-hover': 'var(--fill-transparency-hover)',
49 | 'fill-transparency-pressed': 'var(--fill-transparency-pressed)',
50 | 'scenes-toast': 'var(--scenes-toast)',
51 | 'scenes-overlay': 'var(--scenes-overlay)',
52 | 'scenes-panel': 'var(--scenes-panel)',
53 | 'scenes-guide': 'var(--scenes-guide)',
54 | 'line-1': 'var(--line-1)',
55 | 'line-2': 'var(--line-2)',
56 | 'line-3': 'var(--line-3)',
57 | 'shadow-008': 'var(--shadow-008)',
58 | 'shadow-016': 'var(--shadow-016)',
59 | 'shadow-024': 'var(--shadow-024)',
60 | 'shadow-032': 'var(--shadow-032)',
61 | 'white-inverted': 'var(--white-inverted)',
62 | 'black-inverted': 'var(--black-inverted)',
63 | 'background-1': 'var(--background-1)',
64 | 'background-2': 'var(--background-2)',
65 | 'background-3': 'var(--background-3)',
66 | 'background-4': 'var(--background-4)',
67 | 'background-5': 'var(--background-5)',
68 | 'background-6': 'var(--background-6)',
69 | 'background-7': 'var(--background-7)',
70 | 'background-8': 'var(--background-8)',
71 | 'background-9': 'var(--background-9)',
72 | foreground: 'hsl(var(--foreground))',
73 | grey: {
74 | 50: '#FAFAFA',
75 | 100: '#F4F4F5',
76 | 200: '#E4E4E7',
77 | 300: '#D4D4D8',
78 | 400: '#A1A1AA',
79 | 500: '#71717A',
80 | 600: '#52525B',
81 | 700: '#3F3F46',
82 | 800: '#27272A',
83 | 900: '#222225',
84 | 950: '#19191A',
85 | 1000: '#141415',
86 | 1100: '#0F0F10',
87 | 1200: '#09090B',
88 | 1300: '#050505',
89 | },
90 | green: {
91 | 50: '#E8F5E7',
92 | 100: '#C8E7C4',
93 | 200: '#A4D79E',
94 | 300: '#80C976',
95 | 400: '#63BD58',
96 | 500: '#46B138',
97 | 600: '#3CA22F',
98 | 700: '#2F9024',
99 | 800: '#227F18',
100 | 900: '#006100',
101 | 950: '#006100',
102 | },
103 | primary: {
104 | DEFAULT: 'hsl(var(--primary))',
105 | foreground: 'hsl(var(--primary-foreground))',
106 | },
107 | secondary: {
108 | DEFAULT: 'hsl(var(--secondary))',
109 | foreground: 'hsl(var(--secondary-foreground))',
110 | },
111 | destructive: {
112 | DEFAULT: 'hsl(var(--destructive))',
113 | foreground: 'hsl(var(--destructive-foreground))',
114 | },
115 | muted: {
116 | DEFAULT: 'hsl(var(--muted))',
117 | foreground: 'hsl(var(--muted-foreground))',
118 | },
119 | accent: {
120 | DEFAULT: 'hsl(var(--accent))',
121 | foreground: 'hsl(var(--accent-foreground))',
122 | },
123 | popover: {
124 | DEFAULT: 'hsl(var(--popover))',
125 | foreground: 'hsl(var(--popover-foreground))',
126 | },
127 | card: {
128 | DEFAULT: 'hsl(var(--card))',
129 | foreground: 'hsl(var(--card-foreground))',
130 | },
131 | },
132 | borderRadius: {
133 | lg: 'var(--radius)',
134 | md: 'calc(var(--radius) - 2px)',
135 | sm: 'calc(var(--radius) - 4px)',
136 | },
137 | keyframes: {
138 | 'accordion-down': {
139 | from: { height: '0' },
140 | to: { height: 'var(--radix-accordion-content-height)' },
141 | },
142 | 'accordion-up': {
143 | from: { height: 'var(--radix-accordion-content-height)' },
144 | to: { height: '0' },
145 | },
146 | },
147 | animation: {
148 | 'accordion-down': 'accordion-down 0.2s ease-out',
149 | 'accordion-up': 'accordion-up 0.2s ease-out',
150 | },
151 | },
152 | },
153 | plugins: [require('tailwindcss-animate')],
154 | };
155 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": false,
19 | "noUnusedLocals": false,
20 | "noUnusedParameters": false,
21 | "noFallthroughCasesInSwitch": false,
22 | "baseUrl": ".",
23 | "paths": {
24 | "@/*": ["./src/*"]
25 | }
26 | },
27 | "include": ["src"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import path from 'path';
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | resolve: {
9 | alias: {
10 | '@': path.resolve(__dirname, './src'),
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------