├── .github
└── workflows
│ └── sync.yaml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── components.json
├── eslint.config.js
├── image
└── image.png
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
└── vite.svg
├── src
├── app.tsx
├── assets
│ ├── logo-dark-full.png
│ └── logo-dark.png
├── components
│ ├── button.tsx
│ ├── color-picker
│ │ ├── button.tsx
│ │ ├── color-control.tsx
│ │ ├── color-panel
│ │ │ ├── alpha.tsx
│ │ │ ├── board.tsx
│ │ │ ├── index.tsx
│ │ │ ├── ribbon.tsx
│ │ │ └── types.ts
│ │ ├── colorpicker.css
│ │ ├── constants.ts
│ │ ├── gradient-panel
│ │ │ ├── Markers.tsx
│ │ │ ├── index.tsx
│ │ │ └── types.ts
│ │ ├── gradient
│ │ │ └── index.tsx
│ │ ├── helper.ts
│ │ ├── helpers.ts
│ │ ├── index.tsx
│ │ ├── input.tsx
│ │ ├── popover.tsx
│ │ ├── solid
│ │ │ └── index.tsx
│ │ ├── tabs.tsx
│ │ ├── types.ts
│ │ └── utils
│ │ │ ├── checkFormat.ts
│ │ │ ├── color.ts
│ │ │ ├── getGradient.ts
│ │ │ ├── getHexAlpha.ts
│ │ │ ├── hexToRgba.ts
│ │ │ ├── index.ts
│ │ │ ├── isValidHex.ts
│ │ │ ├── isValidRgba.ts
│ │ │ ├── parseGradient.ts
│ │ │ ├── rgbaToArray.ts
│ │ │ ├── rgbaToHex.ts
│ │ │ ├── useDebounce.ts
│ │ │ └── validGradient.ts
│ ├── featured-testimonials.tsx
│ ├── horizontal-gradient.tsx
│ ├── password.tsx
│ ├── shared
│ │ ├── draggable.tsx
│ │ └── icons.tsx
│ ├── theme-provider.tsx
│ └── ui
│ │ ├── animated-circular-progress.tsx
│ │ ├── animated-tooltip.tsx
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── slider.tsx
│ │ ├── tabs.tsx
│ │ ├── toggle-group.tsx
│ │ └── toggle.tsx
├── constants.ts
├── constants
│ ├── font.ts
│ └── scale.ts
├── data
│ ├── audio.ts
│ ├── fonts.ts
│ ├── images.ts
│ ├── transitions.ts
│ ├── uploads.ts
│ └── video.ts
├── hooks
│ ├── use-current-frame.tsx
│ ├── use-scroll-top.ts
│ └── use-timeline-events.ts
├── index.css
├── interfaces
│ ├── captions.ts
│ ├── editor.ts
│ └── layout.ts
├── lib
│ └── utils.ts
├── main.tsx
├── pages
│ ├── auth
│ │ ├── auth-layout.tsx
│ │ ├── auth.tsx
│ │ └── index.ts
│ └── editor
│ │ ├── control-item
│ │ ├── animations.tsx
│ │ ├── basic-audio.tsx
│ │ ├── basic-image.tsx
│ │ ├── basic-text.tsx
│ │ ├── basic-video.tsx
│ │ ├── common
│ │ │ ├── aspect-ratio.tsx
│ │ │ ├── blur.tsx
│ │ │ ├── brightness.tsx
│ │ │ ├── flip.tsx
│ │ │ ├── opacity.tsx
│ │ │ ├── outline.tsx
│ │ │ ├── playback-rate.tsx
│ │ │ ├── radius.tsx
│ │ │ ├── shadow.tsx
│ │ │ ├── speed.tsx
│ │ │ ├── transform.tsx
│ │ │ └── volume.tsx
│ │ ├── control-item.tsx
│ │ ├── index.tsx
│ │ ├── presets.tsx
│ │ └── smart.tsx
│ │ ├── control-list.tsx
│ │ ├── editor.tsx
│ │ ├── index.ts
│ │ ├── menu-item
│ │ ├── audios.tsx
│ │ ├── captions.tsx
│ │ ├── combo.json
│ │ ├── elements.tsx
│ │ ├── images.tsx
│ │ ├── index.tsx
│ │ ├── menu-item.tsx
│ │ ├── texts.tsx
│ │ ├── transitions.tsx
│ │ ├── uploads.tsx
│ │ └── videos.tsx
│ │ ├── menu-list.tsx
│ │ ├── navbar.tsx
│ │ ├── player
│ │ ├── composition.tsx
│ │ ├── editable-text.tsx
│ │ ├── index.ts
│ │ ├── main-layer-background.tsx
│ │ ├── player.tsx
│ │ └── sequence-item.tsx
│ │ ├── scene.tsx
│ │ ├── timeline
│ │ ├── header.tsx
│ │ ├── index.ts
│ │ ├── items
│ │ │ ├── audio.ts
│ │ │ ├── caption.ts
│ │ │ ├── image.ts
│ │ │ ├── index.ts
│ │ │ ├── text.ts
│ │ │ └── video.ts
│ │ ├── playhead.tsx
│ │ ├── ruler.tsx
│ │ └── timeline.tsx
│ │ └── utils
│ │ ├── fonts.ts
│ │ └── target.ts
├── store
│ ├── store.ts
│ ├── use-auth-store.ts
│ ├── use-data-state.ts
│ └── use-layout-store.ts
├── utils
│ ├── captions.ts
│ ├── download.ts
│ ├── format.ts
│ ├── scene.ts
│ ├── search.ts
│ ├── time.ts
│ ├── timeline.ts
│ ├── upload.ts
│ └── user.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.app.tsbuildinfo
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.node.tsbuildinfo
└── vite.config.ts
/.github/workflows/sync.yaml:
--------------------------------------------------------------------------------
1 | name: Sync
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - master
8 |
9 | permissions:
10 | id-token: write
11 | contents: write
12 |
13 | jobs:
14 | sync:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v3
19 | - name: Connect to AWS
20 | uses: aws-actions/configure-aws-credentials@v1
21 | with:
22 | role-session-name: awssyncsession
23 | role-to-assume: ${{ secrets.AWS_IAM_ROLE }}
24 | aws-region: ${{ secrets.AWS_REGION }}
25 |
26 | - name: sync bucket
27 | run: aws s3 sync ./ s3://${{ secrets.AWS_BUCKET_NAME }} --delete
28 |
--------------------------------------------------------------------------------
/.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 | .env
27 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # remotion-captions
2 |
3 | ## 0.0.12
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies
8 | - @designcombo/timeline@0.1.13
9 | - @designcombo/events@0.1.13
10 | - @designcombo/state@0.1.13
11 | - @designcombo/types@0.1.13
12 |
13 | ## 0.0.11
14 |
15 | ### Patch Changes
16 |
17 | - Updated dependencies
18 | - @designcombo/timeline@0.1.12
19 | - @designcombo/events@0.1.12
20 | - @designcombo/state@0.1.12
21 | - @designcombo/types@0.1.12
22 |
23 | ## 0.0.10
24 |
25 | ### Patch Changes
26 |
27 | - Updated dependencies
28 | - @designcombo/events@0.1.11
29 | - @designcombo/state@0.1.11
30 | - @designcombo/timeline@0.1.11
31 | - @designcombo/types@0.1.11
32 |
33 | ## 0.0.9
34 |
35 | ### Patch Changes
36 |
37 | - Updated dependencies
38 | - @designcombo/timeline@0.1.10
39 | - @designcombo/events@0.1.10
40 | - @designcombo/state@0.1.10
41 | - @designcombo/types@0.1.10
42 |
43 | ## 0.0.8
44 |
45 | ### Patch Changes
46 |
47 | - Updated dependencies
48 | - @designcombo/timeline@0.1.9
49 | - @designcombo/events@0.1.9
50 | - @designcombo/state@0.1.9
51 | - @designcombo/types@0.1.9
52 |
53 | ## 0.0.7
54 |
55 | ### Patch Changes
56 |
57 | - Updated dependencies
58 | - @designcombo/events@0.1.7
59 | - @designcombo/state@0.1.7
60 | - @designcombo/timeline@0.1.7
61 | - @designcombo/types@0.1.7
62 |
63 | ## 0.0.6
64 |
65 | ### Patch Changes
66 |
67 | - Updated dependencies
68 | - @designcombo/events@0.1.6
69 | - @designcombo/state@0.1.6
70 | - @designcombo/timeline@0.1.6
71 | - @designcombo/types@0.1.6
72 |
73 | ## 0.0.5
74 |
75 | ### Patch Changes
76 |
77 | - Updated dependencies
78 | - @designcombo/timeline@0.1.5
79 | - @designcombo/events@0.1.5
80 | - @designcombo/state@0.1.5
81 | - @designcombo/types@0.1.5
82 |
83 | ## 0.0.4
84 |
85 | ### Patch Changes
86 |
87 | - Updated dependencies
88 | - @designcombo/timeline@1.0.0
89 | - @designcombo/events@0.2.0
90 | - @designcombo/state@1.0.0
91 | - @designcombo/types@0.2.0
92 |
93 | ## 0.0.3
94 |
95 | ### Patch Changes
96 |
97 | - Updated dependencies
98 | - @designcombo/timeline@1.0.0
99 | - @designcombo/events@0.2.0
100 | - @designcombo/state@1.0.0
101 | - @designcombo/types@0.2.0
102 |
103 | ## 0.0.2
104 |
105 | ### Patch Changes
106 |
107 | - Updated dependencies
108 | - @designcombo/events@0.1.4
109 | - @designcombo/state@0.1.4
110 | - @designcombo/timeline@0.1.4
111 | - @designcombo/types@0.1.4
112 |
113 | ## 0.0.1
114 |
115 | ### Patch Changes
116 |
117 | - Updated dependencies
118 | - @designcombo/events@0.1.3
119 | - @designcombo/state@0.1.3
120 | - @designcombo/timeline@0.1.3
121 | - @designcombo/types@0.1.3
122 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Video editor in React JS
2 |
3 | The project is for video editing through tools such as DesignCombo, ShaCDN, React, etc. In order to improve the quality of the user experience that comes with the use of these very heavy software, all this in a light and web-based way.
4 |
5 | ## Authors
6 |
7 | - [@Pablituuu](https://www.github.com/Pablituuu)
8 |
9 | ## Installation
10 |
11 | Install react-video-editor with npm
12 |
13 | ```bash
14 | git clone https://github.com/Pablituuu/react-video-editor.git
15 | cd react-video-editor
16 | npm install
17 | npm run dev
18 | ```
19 |
20 | ## Tech Stack
21 |
22 | **Client:** React JS, TailwindCSS, ShaCDN, DesignCombo, Zustand
23 |
24 | ## License
25 |
26 | [MIT](https://choosealicense.com/licenses/mit/)
27 |
28 | ## Demo
29 |
30 | https://react-video-editor-mu.vercel.app/
31 | {width=50%}
32 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/image/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pablituuu/react-video-editor/e474842e5fee4476905dd175b85b529a7fb9f6d7/image/image.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remotion-captions",
3 | "private": true,
4 | "version": "0.0.12",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@designcombo/events": "0.1.14",
14 | "@designcombo/state": "0.1.14",
15 | "@designcombo/timeline": "0.1.14",
16 | "@designcombo/types": "0.1.14",
17 | "@emotion/react": "^11.13.3",
18 | "@emotion/styled": "^11.13.0",
19 | "@hookform/resolvers": "^3.4.2",
20 | "@interactify/infinite-viewer": "^0.0.2",
21 | "@interactify/moveable": "0.0.2",
22 | "@interactify/selection": "^0.1.0",
23 | "@radix-ui/react-avatar": "^1.1.0",
24 | "@radix-ui/react-dropdown-menu": "^2.1.1",
25 | "@radix-ui/react-icons": "^1.3.0",
26 | "@radix-ui/react-label": "^2.1.0",
27 | "@radix-ui/react-popover": "^1.1.1",
28 | "@radix-ui/react-progress": "^1.1.0",
29 | "@radix-ui/react-scroll-area": "^1.1.0",
30 | "@radix-ui/react-slider": "^1.2.0",
31 | "@radix-ui/react-slot": "^1.0.2",
32 | "@radix-ui/react-tabs": "^1.1.0",
33 | "@radix-ui/react-toggle": "^1.1.0",
34 | "@radix-ui/react-toggle-group": "^1.1.0",
35 | "@remotion/player": "^4.0.212",
36 | "@tabler/icons-react": "^3.5.0",
37 | "@types/tinycolor2": "^1.4.6",
38 | "axios": "^1.7.7",
39 | "class-variance-authority": "^0.7.0",
40 | "clsx": "^2.1.1",
41 | "framer-motion": "^11.5.6",
42 | "lodash": "^4.17.21",
43 | "lodash-es": "^4.17.21",
44 | "lucide-react": "^0.441.0",
45 | "non.geist": "^1.0.2",
46 | "react": "^18.3.1",
47 | "react-dom": "^18.3.1",
48 | "react-hook-form": "^7.51.5",
49 | "react-resizable-panels": "^2.1.3",
50 | "react-router-dom": "^6.26.2",
51 | "remotion": "^4.0.212",
52 | "tailwind-merge": "^2.5.2",
53 | "tailwindcss-animate": "^1.0.7",
54 | "tinycolor2": "^1.6.0",
55 | "zod": "^3.23.8",
56 | "zustand": "^4.5.5"
57 | },
58 | "devDependencies": {
59 | "@eslint/js": "^9.9.0",
60 | "@types/lodash": "^4.17.9",
61 | "@types/node": "^22.5.5",
62 | "@types/react": "^18.3.3",
63 | "@types/react-dom": "^18.3.0",
64 | "@vitejs/plugin-react": "^4.3.1",
65 | "autoprefixer": "^10.4.20",
66 | "eslint": "^9.9.0",
67 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
68 | "eslint-plugin-react-refresh": "^0.4.9",
69 | "globals": "^15.9.0",
70 | "postcss": "^8.4.47",
71 | "tailwindcss": "^3.4.12",
72 | "typescript": "^5.5.3",
73 | "typescript-eslint": "^8.0.1",
74 | "vite": "^5.4.1"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import Editor from "./pages/editor";
3 | import useAuthStore from "./store/use-auth-store";
4 | import useDataState from "./store/use-data-state";
5 | import { getCompactFontData } from "./pages/editor/utils/fonts";
6 | import { FONTS } from "./data/fonts";
7 |
8 | export default function App() {
9 | const { user } = useAuthStore();
10 | const { setCompactFonts, setFonts } = useDataState();
11 |
12 | useEffect(() => {
13 | setCompactFonts(getCompactFontData(FONTS));
14 | setFonts(FONTS);
15 | }, []);
16 |
17 | useEffect(() => {
18 | if (user?.id) {
19 | }
20 | }, [user?.id]);
21 |
22 | return ;
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/logo-dark-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pablituuu/react-video-editor/e474842e5fee4476905dd175b85b529a7fb9f6d7/src/assets/logo-dark-full.png
--------------------------------------------------------------------------------
/src/assets/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pablituuu/react-video-editor/e474842e5fee4476905dd175b85b529a7fb9f6d7/src/assets/logo-dark.png
--------------------------------------------------------------------------------
/src/components/button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import React from "react";
3 |
4 | export const Button: React.FC<{
5 | children?: React.ReactNode;
6 | className?: string;
7 | variant?: "simple" | "outline" | "primary";
8 | as?: React.ElementType;
9 | [x: string]: any;
10 | }> = ({
11 | children,
12 | className,
13 | variant = "primary",
14 | as: Tag = "button",
15 | ...props
16 | }) => {
17 | const variantClass =
18 | variant === "simple"
19 | ? "bg-black relative z-10 bg-transparent hover:bg-gray-100 border border-transparent text-black text-sm md:text-sm transition font-medium duration-200 rounded-full px-4 py-2 flex items-center justify-center dark:text-white dark:hover:bg-neutral-800 dark:hover:shadow-xl"
20 | : variant === "outline"
21 | ? "bg-white relative z-10 hover:bg-black/90 hover:shadow-xl text-black border border-black hover:text-white text-sm md:text-sm transition font-medium duration-200 rounded-full px-4 py-2 flex items-center justify-center"
22 | : variant === "primary"
23 | ? "bg-neutral-900 relative z-10 hover:bg-black/90 border border-transparent text-white text-sm md:text-sm transition font-medium duration-200 rounded-full px-4 py-2 flex items-center justify-center shadow-[0px_-1px_0px_0px_#FFFFFF40_inset,_0px_1px_0px_0px_#FFFFFF40_inset]"
24 | : "";
25 | return (
26 |
34 | {children ?? `Get Started`}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/color-picker/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 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/color-picker/color-control.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect, useState } from "react";
2 | import tinycolor from "tinycolor2";
3 | import { checkFormat } from "./utils";
4 | import { getAlphaValue, onlyDigits, onlyLatins } from "./helpers";
5 | // import { Input } from "./input";
6 | import { Popover, PopoverContent, PopoverTrigger } from "./popover";
7 | import { Input } from "../ui/input";
8 |
9 | interface IChange {
10 | hex: string;
11 | alpha: number;
12 | }
13 |
14 | type TProps = {
15 | hex: string;
16 | alpha: number;
17 | format?: "rgb" | "hsl" | "hex";
18 | onChange: ({ hex, alpha }: IChange) => void;
19 | onSubmitChange?: (rgba: string) => void;
20 | };
21 |
22 | const InputRgba: FC = ({
23 | hex,
24 | alpha,
25 | format = "rgb",
26 | onChange,
27 | onSubmitChange
28 | }) => {
29 | const [color, setColor] = useState({
30 | alpha,
31 | hex
32 | });
33 |
34 | const onChangeAlpha = (alpha: string) => {
35 | const validAlpha = getAlphaValue(alpha);
36 |
37 | setColor({
38 | ...color,
39 | alpha: Number(validAlpha)
40 | });
41 | };
42 |
43 | const onChangeHex = (hex: string) => {
44 | setColor({
45 | ...color,
46 | hex
47 | });
48 | };
49 |
50 | const onHandleSubmit = () => {
51 | const rgba = tinycolor(color.hex[0] === "#" ? color.hex : "#" + color.hex);
52 | rgba.setAlpha(Number(color.alpha) / 100);
53 |
54 | if (rgba && (color.alpha !== alpha || color.hex !== hex)) {
55 | onChange({
56 | hex: color.hex[0] === "#" ? color.hex : "#" + color.hex,
57 | alpha: Number(color.alpha)
58 | });
59 | if (onSubmitChange) {
60 | onSubmitChange(checkFormat(rgba.toRgbString(), format, color.alpha));
61 | }
62 | } else {
63 | setColor({
64 | hex,
65 | alpha
66 | });
67 | onChange({
68 | hex,
69 | alpha
70 | });
71 | }
72 | };
73 |
74 | useEffect(() => {
75 | setColor({
76 | hex,
77 | alpha
78 | });
79 | }, [hex, alpha]);
80 |
81 | return (
82 |
88 |
89 |
90 |
91 | Hex
92 |
102 |
103 |
104 |
105 |
106 | Hex
107 |
108 |
109 |
onChangeHex(onlyLatins(e.target.value))}
113 | onBlur={onHandleSubmit}
114 | onKeyDown={(e) => {
115 | if (e.key === "Enter") {
116 | onHandleSubmit();
117 | }
118 | }}
119 | className="pl-[70px]"
120 | />
121 |
122 |
123 |
onChangeAlpha(onlyDigits(e.target.value))}
127 | onBlur={onHandleSubmit}
128 | onKeyDown={(e) => {
129 | if (e.key === "Enter") {
130 | onHandleSubmit();
131 | }
132 | }}
133 | className="pl-2 "
134 | />
135 |
136 | %
137 |
138 |
139 |
140 | );
141 | };
142 |
143 | export default InputRgba;
144 |
--------------------------------------------------------------------------------
/src/components/color-picker/color-panel/index.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState, useRef, useEffect, MutableRefObject } from "react";
2 | import Board from "./board";
3 | import Ribbon from "./ribbon";
4 | import Alpha from "./alpha";
5 |
6 | import TinyColor, { ITinyColor } from "../utils/color";
7 | import { TPropsMain } from "./types";
8 |
9 | const Panel: FC = ({ alpha, hex, colorBoardHeight, onChange }) => {
10 | const node = useRef() as MutableRefObject;
11 |
12 | const colorConvert = new TinyColor(hex) as ITinyColor;
13 | colorConvert.alpha = alpha;
14 | const [state, setState] = useState({
15 | color: colorConvert,
16 | alpha
17 | });
18 | const [change, setChange] = useState(false);
19 |
20 | useEffect(() => {
21 | if (!change) {
22 | setState({
23 | color: colorConvert,
24 | alpha
25 | });
26 | }
27 | // eslint-disable-next-line react-hooks/exhaustive-deps
28 | }, [hex, alpha]);
29 |
30 | const handleAlphaChange = (alpha: number) => {
31 | setChange(true);
32 | const { color } = state;
33 | color.alpha = alpha;
34 |
35 | setState({
36 | color,
37 | alpha
38 | });
39 | onChange({
40 | hex: color.toHexString(),
41 | alpha
42 | });
43 | };
44 |
45 | const handleChange = (color: ITinyColor) => {
46 | setChange(true);
47 | const { alpha } = state;
48 | color.alpha = alpha;
49 |
50 | setState({ ...state, color, alpha: color.alpha });
51 | onChange({
52 | hex: color.toHexString(),
53 | alpha: color.alpha
54 | });
55 | };
56 |
57 | return (
58 |
87 | );
88 | };
89 |
90 | export default Panel;
91 |
--------------------------------------------------------------------------------
/src/components/color-picker/color-panel/types.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from "react";
2 |
3 | import { ITinyColor } from "../utils/color";
4 |
5 | export type TPropsChange = {
6 | alpha: number;
7 | hex: string;
8 | };
9 |
10 | export type TPropsComp = {
11 | rootPrefixCls?: string;
12 | color: ITinyColor;
13 | alpha?: number;
14 | colorBoardHeight?: number;
15 | onChange: (color: ITinyColor) => void;
16 | setChange: Dispatch>;
17 | };
18 |
19 | export type TPropsCompAlpha = {
20 | color: ITinyColor;
21 | alpha?: number;
22 | onChange: (alpha: number) => void;
23 | setChange: Dispatch>;
24 | };
25 |
26 | export type TPropsMain = {
27 | alpha: number;
28 | className?: string;
29 | hex: string;
30 | colorBoardHeight?: number;
31 | onChange: ({ alpha, hex }: TPropsChange) => void;
32 | };
33 |
34 | export type TCoords = {
35 | x: number;
36 | y: number;
37 | };
38 |
--------------------------------------------------------------------------------
/src/components/color-picker/colorpicker.css:
--------------------------------------------------------------------------------
1 | .gradient-result {
2 | height: 74px;
3 | width: 100%;
4 | position: relative;
5 | border-radius: 6px;
6 | flex-grow: 1;
7 | font-size: 16px;
8 | }
9 |
10 | .gradient-result:hover .gradient-angle {
11 | opacity: 1;
12 | }
13 |
14 | .gradient-mode {
15 | height: 32px;
16 | width: 32px;
17 | position: relative;
18 | top: 20px;
19 | left: 16px;
20 | border: 2px solid white;
21 | border-radius: 0.15em;
22 | cursor: pointer;
23 | opacity: 0.25;
24 | transition: all 0.3s;
25 | }
26 |
27 | .gradient-mode::before {
28 | position: absolute;
29 | content: "";
30 | top: 0;
31 | right: 0;
32 | bottom: 0;
33 | left: 0;
34 | margin: auto;
35 | transition: all 0.3s;
36 | }
37 |
38 | .gradient-mode:hover {
39 | opacity: 1;
40 | }
41 |
42 | .gradient-mode[data-mode="linear"]::before {
43 | height: 2px;
44 | width: 70%;
45 | background: white;
46 | transform: rotate(45deg);
47 | border-radius: 50em;
48 | }
49 |
50 | .gradient-mode[data-mode="radial"]::before {
51 | height: 50%;
52 | width: 50%;
53 | border-radius: 100%;
54 | border: 2px solid white;
55 | background-color: white;
56 | }
57 |
58 | .gradient-mode[data-mode="radial"] + .gradient-angle {
59 | opacity: 0;
60 | }
61 |
62 | .gradient-angle {
63 | height: 0.35em;
64 | width: 0.35em;
65 | background: white;
66 | border-radius: 100%;
67 | top: 0;
68 | right: 0;
69 | bottom: 0;
70 | left: 0;
71 | transition: all 0.3s;
72 | position: absolute;
73 | margin: auto;
74 | opacity: 0.25;
75 | }
76 |
77 | .gradient-angle > div {
78 | height: 2px;
79 | width: 2em;
80 | top: 0;
81 | right: 0;
82 | bottom: 0;
83 | left: 50%;
84 | position: absolute;
85 | background: white;
86 | border-radius: 1em;
87 | margin: auto 0;
88 | transform-origin: left;
89 | }
90 |
91 | .gradient-pos {
92 | height: 5em;
93 | width: 5em;
94 | display: grid;
95 | grid-template-columns: 1fr 1fr 1fr;
96 | grid-template-rows: 1fr 1fr 1fr;
97 | opacity: 1;
98 | top: 0;
99 | right: 0;
100 | bottom: 0;
101 | left: 0;
102 | transition: all 0.3s;
103 | position: absolute;
104 | margin: auto;
105 | }
106 |
107 | .gradient-pos > div {
108 | height: 15px;
109 | width: 15px;
110 | border: 2px solid transparent;
111 | position: relative;
112 | margin: auto;
113 | transition: all 0.3s;
114 | }
115 |
116 | .gradient-pos > div:not(.gradient-active) {
117 | cursor: pointer;
118 | }
119 |
120 | .gradient-pos > div::before {
121 | position: absolute;
122 | content: "";
123 | top: 0;
124 | right: 0;
125 | bottom: 0;
126 | left: 0;
127 | height: 5px;
128 | width: 5px;
129 | border-radius: 100%;
130 | background: white;
131 | transition: all 0.3s;
132 | opacity: 0.25;
133 | margin: auto;
134 | }
135 |
136 | .gradient-pos > div:hover::before {
137 | opacity: 1;
138 | }
139 |
140 | .gradient-pos > div.gradient-active {
141 | border-color: white;
142 | border-radius: 100%;
143 | }
144 |
145 | .gradient-pos > div.gradient-active::before {
146 | opacity: 1;
147 | }
148 |
--------------------------------------------------------------------------------
/src/components/color-picker/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_COLORS = [
2 | '#FF6900',
3 | '#FCB900',
4 | '#7BDCB5',
5 | '#00D084',
6 | '#8ED1FC',
7 | '#0693E3',
8 | '#ABB8C3',
9 | '#607d8b',
10 | '#EB144C',
11 | '#F78DA7',
12 | '#ba68c8',
13 | '#9900EF',
14 | 'linear-gradient(0deg, rgb(255, 177, 153) 0%, rgb(255, 8, 68) 100%)',
15 | 'linear-gradient(270deg, rgb(251, 171, 126) 8.00%, rgb(247, 206, 104) 92.00%)',
16 | 'linear-gradient(315deg, rgb(150, 230, 161) 8.00%, rgb(212, 252, 121) 92.00%)',
17 | 'linear-gradient(to left, rgb(249, 240, 71) 0%, rgb(15, 216, 80) 100%)',
18 | 'linear-gradient(315deg, rgb(194, 233, 251) 8.00%, rgb(161, 196, 253) 92.00%)',
19 | 'linear-gradient(0deg, rgb(0, 198, 251) 0%, rgb(0, 91, 234) 100%)',
20 | 'linear-gradient(0deg, rgb(167, 166, 203) 0%, rgb(137, 137, 186) 51.00%, rgb(137, 137, 186) 100%)',
21 | 'linear-gradient(0deg, rgb(80, 82, 133) 0%, rgb(88, 94, 146) 15.0%, rgb(101, 104, 159) 28.00%, rgb(116, 116, 176) 43.00%, rgb(126, 126, 187) 57.00%, rgb(131, 137, 199) 71.00%, rgb(151, 149, 212) 82.00%, rgb(162, 161, 220) 92.00%, rgb(181, 174, 228) 100%)',
22 | 'linear-gradient(270deg, rgb(255, 126, 179) 0%, rgb(255, 117, 140) 100%)',
23 | 'linear-gradient(90deg, rgb(120, 115, 245) 0%, rgb(236, 119, 171) 100%)',
24 | 'linear-gradient(45deg, #2e266f 0.00%, #9664dd38 100.00%)',
25 | 'radial-gradient(circle at center, yellow 0%, #009966 50%, purple 100%)'
26 | ];
27 |
28 | export const RADIALS_POS = [
29 | { pos: 'tl', css: 'circle at left top', active: false },
30 | { pos: 'tm', css: 'circle at center top', active: false },
31 | { pos: 'tr', css: 'circle at right top', active: false },
32 |
33 | { pos: 'l', css: 'circle at left', active: false },
34 | { pos: 'm', css: 'circle at center', active: true },
35 | { pos: 'r', css: 'circle at right', active: false },
36 |
37 | { pos: 'bl', css: 'circle at left bottom', active: false },
38 | { pos: 'bm', css: 'circle at center bottom', active: false },
39 | { pos: 'br', css: 'circle at right bottom', active: false }
40 | ];
41 |
--------------------------------------------------------------------------------
/src/components/color-picker/gradient-panel/types.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from "react";
2 |
3 | import { IActiveColor } from "../types";
4 |
5 | export interface IColor {
6 | gradient: string;
7 | type: string;
8 | modifier: string | number;
9 | stops: Array;
10 | }
11 |
12 | export type TCoords = {
13 | x: number;
14 | y: number;
15 | shiftKey?: number | boolean;
16 | ctrlKey?: number | boolean;
17 | };
18 |
19 | export interface IPropsPanel {
20 | color: IColor;
21 | setColor: (color: IColor) => void;
22 | activeColor: IActiveColor;
23 | setActiveColor: Dispatch>;
24 | setInit: Dispatch>;
25 | showGradientResult?: boolean;
26 | showGradientStops?: boolean;
27 | showGradientMode?: boolean;
28 | showGradientAngle?: boolean;
29 | showGradientPosition?: boolean;
30 | allowAddGradientStops?: boolean;
31 | format?: "rgb" | "hsl" | "hex";
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/color-picker/helper.ts:
--------------------------------------------------------------------------------
1 | import tinycolor from "tinycolor2";
2 |
3 | import { rgbaToArray, isValidRgba, validGradient } from "./utils";
4 |
5 | export const getIndexActiveTag = (value: string) => {
6 | let tab = "solid";
7 | const validValue = tinycolor(value).isValid();
8 |
9 | if (value) {
10 | if (value === "transparent") {
11 | tab = "solid";
12 | return tab;
13 | }
14 | if (
15 | validValue &&
16 | !value.trim().startsWith("radial-gradient") &&
17 | !value.trim().startsWith("linear-gradient")
18 | ) {
19 | tab = "solid";
20 | return tab;
21 | }
22 | const rgba = rgbaToArray(value);
23 | if (rgba) {
24 | if (isValidRgba([rgba[0], rgba[1], rgba[2]])) {
25 | tab = "solid";
26 | return tab;
27 | }
28 | } else {
29 | tab = "gradient";
30 | return tab;
31 | }
32 | }
33 |
34 | return tab;
35 | };
36 |
37 | export const checkValidColorsArray = (
38 | arr: string[],
39 | type: "solid" | "grad"
40 | ) => {
41 | if (!arr.length || !Array.isArray(arr)) {
42 | return [];
43 | }
44 |
45 | const uniqueArr = [...new Set(arr)];
46 |
47 | switch (type) {
48 | case "solid":
49 | return uniqueArr.filter((color: string, index: number) => {
50 | const tinyColor = tinycolor(color);
51 | if (
52 | tinyColor.isValid() &&
53 | !color.trim().startsWith("radial-gradient") &&
54 | !color.trim().startsWith("linear-gradient")
55 | ) {
56 | return true;
57 | }
58 |
59 | if (index > 100) {
60 | return false;
61 | }
62 |
63 | return false;
64 | });
65 | case "grad":
66 | return uniqueArr.filter((color: string, index: number) => {
67 | const validColor = validGradient(color);
68 |
69 | if (validColor === "Failed to find gradient") {
70 | return false;
71 | }
72 |
73 | if (validColor === "Not correct position") {
74 | console.warn(
75 | "Incorrect gradient default value. You need to indicate the location for the colors. We ignore this gradient value"
76 | );
77 | return false;
78 | }
79 |
80 | if (index > 100) {
81 | return false;
82 | }
83 |
84 | return true;
85 | });
86 |
87 | default:
88 | return [];
89 | }
90 | };
91 |
92 | export const arraysEqual = (a: Array, b: Array) => {
93 | if (a instanceof Array && b instanceof Array) {
94 | if (a.length !== b.length) return false;
95 | for (let i = 0; i < a.length; i++)
96 | if (!arraysEqual(a[i], b[i])) return false;
97 | return true;
98 | } else {
99 | return a === b;
100 | }
101 | };
102 |
103 | export const shallowEqual = (object1: any, object2: any) => {
104 | const keys1 = Object.keys(object1);
105 | const keys2 = Object.keys(object2);
106 |
107 | if (keys1.length !== keys2.length) {
108 | return false;
109 | }
110 |
111 | for (const key of keys1) {
112 | if (object1[key] !== object2[key]) {
113 | return false;
114 | }
115 | }
116 |
117 | return true;
118 | };
119 |
--------------------------------------------------------------------------------
/src/components/color-picker/helpers.ts:
--------------------------------------------------------------------------------
1 | export const getAlphaValue = (value: string) => {
2 | value = value.replace(/%/i, ""); // Ensure to assign the result back
3 | if (value[0] === "0" && value.length > 1) {
4 | return value.substring(1); // Replaced substr with substring
5 | } else if (Number(value) >= 100) {
6 | return 100;
7 | } else if (!isNaN(Number(value))) {
8 | return value || 0;
9 | }
10 | return parseInt(value);
11 | };
12 |
13 | export const onlyDigits = (string: string) => {
14 | return string ? string.substring(0, 3).replace(/[^\d]/g, "") : ""; // Replaced substr with substring
15 | };
16 |
17 | export const onlyLatins = (string: string) => {
18 | return string ? string.substring(0, 7) : string;
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/color-picker/index.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, FC } from "react";
2 | import Gradient from "./gradient";
3 | import Solid from "./solid";
4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
5 | import { IPropsMain } from "./types";
6 | import "./colorpicker.css";
7 |
8 | const ColorPicker: FC = ({
9 | value = "#ffffff",
10 | format = "rgb",
11 | gradient = false,
12 | solid = true,
13 | debounceMS = 300,
14 | debounce = true,
15 | showInputs = true,
16 | showGradientResult = true,
17 | showGradientStops = true,
18 | showGradientMode = true,
19 | showGradientAngle = true,
20 | showGradientPosition = true,
21 | allowAddGradientStops = true,
22 | colorBoardHeight = 140,
23 |
24 | onChange = () => ({})
25 | }) => {
26 | const onChangeSolid = (value: string) => {
27 | onChange(value);
28 | };
29 |
30 | const onChangeGradient = (value: string) => {
31 | onChange(value);
32 | };
33 |
34 | if (solid && gradient) {
35 | return (
36 |
37 |
38 |
39 | Solid
40 | Gradient
41 |
42 |
43 |
51 |
52 |
53 |
67 |
68 |
69 |
70 | );
71 | }
72 |
73 | return (
74 | <>
75 | {solid || gradient ? (
76 | <>
77 | {solid ? (
78 |
87 | ) : (
88 |
89 | )}
90 | {gradient ? (
91 |
105 | ) : (
106 |
107 | )}
108 | >
109 | ) : null}
110 | >
111 | );
112 | };
113 |
114 | export default ColorPicker;
115 |
--------------------------------------------------------------------------------
/src/components/color-picker/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/color-picker/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/color-picker/solid/index.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect, useRef, useState } from "react";
2 | import tinycolor from "tinycolor2";
3 |
4 | import ColorPickerPanel from "../color-panel";
5 | import InputRgba from "../color-control";
6 |
7 | import { getHexAlpha, useDebounce, checkFormat } from "../utils";
8 |
9 | import { IPropsComp, TPropsChange } from "../types";
10 |
11 | const ColorPickerSolid: FC = ({
12 | value = "#ffffff",
13 | onChange = () => ({}),
14 | format = "rgb",
15 | debounceMS = 300,
16 | debounce = true,
17 | colorBoardHeight = 180,
18 | }) => {
19 | const node = useRef(null);
20 |
21 | const [init, setInit] = useState(true);
22 | const [color, setColor] = useState(getHexAlpha(value));
23 |
24 | const debounceColor = useDebounce(color, debounceMS);
25 |
26 | useEffect(() => {
27 | if (debounce && debounceColor && init === false) {
28 | if (value === "transparent" && color.alpha === 0) {
29 | color.alpha = 100;
30 | }
31 |
32 | const rgba = tinycolor(color.hex);
33 | rgba.setAlpha(color.alpha / 100);
34 | if (tinycolor(rgba).toRgbString() === tinycolor(value).toRgbString()) {
35 | return;
36 | }
37 |
38 | onChange(checkFormat(rgba.toRgbString(), format, debounceColor.alpha));
39 | }
40 | // eslint-disable-next-line react-hooks/exhaustive-deps
41 | }, [debounceColor]);
42 |
43 | // Issue https://github.com/undind/react-gcolor-picker/issues/6
44 | useEffect(() => {
45 | setColor(getHexAlpha(value));
46 | }, [value]);
47 |
48 | const onCompleteChange = (value: TPropsChange) => {
49 | setInit(false);
50 | setColor({
51 | hex: value.hex,
52 | alpha: Math.round(value.alpha),
53 | });
54 | };
55 |
56 | return (
57 |
58 |
64 |
71 |
72 | );
73 | };
74 |
75 | export default ColorPickerSolid;
76 |
--------------------------------------------------------------------------------
/src/components/color-picker/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/color-picker/types.ts:
--------------------------------------------------------------------------------
1 | export interface IPropsComp {
2 | value: string;
3 | format?: "rgb" | "hsl" | "hex";
4 | debounceMS?: number;
5 | debounce?: boolean;
6 | showInputs?: boolean;
7 | showGradientResult?: boolean;
8 | showGradientStops?: boolean;
9 | showGradientMode?: boolean;
10 | showGradientAngle?: boolean;
11 | showGradientPosition?: boolean;
12 | allowAddGradientStops?: boolean;
13 | colorBoardHeight?: number;
14 | defaultColors?: string[];
15 | defaultActiveTab?: string | undefined;
16 | onChangeTabs?: (tab: string) => void;
17 | onChange: (value: string) => void;
18 | }
19 |
20 | export interface IPropsMain extends IPropsComp {
21 | gradient?: boolean;
22 | solid?: boolean;
23 | popupWidth?: number;
24 | }
25 |
26 | export type TPropsChange = {
27 | alpha: number;
28 | hex: string;
29 | };
30 |
31 | export interface IActiveColor {
32 | hex: string;
33 | alpha: number;
34 | loc: any;
35 | index: any;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/checkFormat.ts:
--------------------------------------------------------------------------------
1 | import tinycolor from "tinycolor2";
2 |
3 | export default (color: string, format: string, stateColorAlpha?: number) => {
4 | const tinyColor = tinycolor(color);
5 | let value: string;
6 | const alphaValue = stateColorAlpha || tinyColor.getAlpha() * 100;
7 |
8 | switch (format) {
9 | case "rgb":
10 | value = tinyColor.toRgbString();
11 | break;
12 | case "hsl":
13 | value = tinyColor.toHslString();
14 | break;
15 | case "hex":
16 | if (alphaValue !== 100) {
17 | value = tinyColor.toHex8String();
18 | } else {
19 | value = tinyColor.toHexString();
20 | }
21 | break;
22 |
23 | default:
24 | value = "";
25 | break;
26 | }
27 |
28 | return value;
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/getGradient.ts:
--------------------------------------------------------------------------------
1 | import checkFormat from "./checkFormat";
2 |
3 | export default (
4 | type: string,
5 | stops: Array,
6 | modifier: string | number | undefined,
7 | format: "rgb" | "hsl" | "hex" = "rgb"
8 | ) => {
9 | let str = "";
10 |
11 | switch (type) {
12 | case "linear":
13 | if (typeof modifier === "number") {
14 | str = `linear-gradient(${modifier}deg, ${stops.map(
15 | (color: [string, number]) => {
16 | return `${checkFormat(color[0], format)} ${Math.round(
17 | color[1] * 100
18 | ).toFixed(2)}%`;
19 | }
20 | )})`;
21 | }
22 | if (typeof modifier === "string") {
23 | str = `linear-gradient(${modifier}, ${stops.map(
24 | (color: [string, number]) => {
25 | return `${checkFormat(color[0], format)} ${Math.round(
26 | color[1] * 100
27 | ).toFixed(2)}%`;
28 | }
29 | )})`;
30 | }
31 | break;
32 | case "radial":
33 | str = `radial-gradient(${modifier}, ${stops.map(
34 | (color: [string, number]) => {
35 | return `${checkFormat(color[0], format)} ${Math.round(
36 | color[1] * 100
37 | ).toFixed(2)}%`;
38 | }
39 | )})`;
40 | break;
41 | default:
42 | break;
43 | }
44 |
45 | return str;
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/getHexAlpha.ts:
--------------------------------------------------------------------------------
1 | import tinycolor from 'tinycolor2';
2 |
3 | export default (value: string) => {
4 | const defaultObject = {
5 | hex: '#ffffff',
6 | alpha: 100
7 | };
8 | const tinyColor = tinycolor(value);
9 |
10 | if (value) {
11 | if (
12 | tinyColor.isValid() &&
13 | !value.trim().startsWith('radial-gradient') &&
14 | !value.trim().startsWith('linear-gradient')
15 | ) {
16 | defaultObject.hex = tinyColor.toHexString();
17 | defaultObject.alpha = Math.round(tinyColor.getAlpha() * 100);
18 | } else {
19 | return defaultObject;
20 | }
21 | }
22 |
23 | return defaultObject;
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/hexToRgba.ts:
--------------------------------------------------------------------------------
1 | export default (hexVal: string, opacityVal: number) => {
2 | const opacity = isNaN(opacityVal) ? 100 : opacityVal;
3 | const hex = hexVal.replace('#', '');
4 | let r;
5 | let g;
6 | let b;
7 |
8 | if (hex.length === 6) {
9 | r = parseInt(hex.substring(0, 2), 16);
10 | g = parseInt(hex.substring(2, 4), 16);
11 | b = parseInt(hex.substring(4, 6), 16);
12 | } else {
13 | const rd = hex.substring(0, 1) + hex.substring(0, 1);
14 | const gd = hex.substring(1, 2) + hex.substring(1, 2);
15 | const bd = hex.substring(2, 3) + hex.substring(2, 3);
16 | r = parseInt(rd, 16);
17 | g = parseInt(gd, 16);
18 | b = parseInt(bd, 16);
19 | }
20 |
21 | return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + opacity / 100 + ')';
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as hexToRgba } from './hexToRgba';
2 | export { default as getHexAlpha } from './getHexAlpha';
3 | export { default as useDebounce } from './useDebounce';
4 | export { default as parseGradient } from './parseGradient';
5 | export { default as getGradient } from './getGradient';
6 | export { default as rgbaToArray } from './rgbaToArray';
7 | export { default as rgbaToHex } from './rgbaToHex';
8 | export { default as isValidHex } from './isValidHex';
9 | export { default as isValidRgba } from './isValidRgba';
10 | export { default as checkFormat } from './checkFormat';
11 | export { default as validGradient } from './validGradient';
12 | export { default as TinyColor } from './color';
13 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/isValidHex.ts:
--------------------------------------------------------------------------------
1 | export default (hex: string) => {
2 | const validHex = new RegExp(
3 | /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i
4 | );
5 |
6 | return validHex.test(hex);
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/isValidRgba.ts:
--------------------------------------------------------------------------------
1 | import rgbaToHex from './rgbaToHex';
2 |
3 | export default (rgba: Array) => {
4 | return !!rgbaToHex(rgba);
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/parseGradient.ts:
--------------------------------------------------------------------------------
1 | import tinycolor from 'tinycolor2';
2 |
3 | import { validGradient } from '.';
4 |
5 | interface IGradientStop {
6 | color: string;
7 | position?: number;
8 | }
9 |
10 | const LINEAR_POS = [
11 | { angle: '0', name: 'to top' },
12 | { angle: '45', name: 'to top right' },
13 | { angle: '45', name: 'to right top' },
14 | { angle: '90', name: 'to right' },
15 | { angle: '135', name: 'to right bottom' },
16 | { angle: '135', name: 'to bottom right' },
17 | { angle: '180', name: 'to bottom' },
18 | { angle: '225', name: 'to left bottom' },
19 | { angle: '225', name: 'to bottom left' },
20 | { angle: '270', name: 'to left' },
21 | { angle: '315', name: 'to top left' },
22 | { angle: '315', name: 'to left top' }
23 | ];
24 |
25 | export default (str: string) => {
26 | const tinyColor = tinycolor(str);
27 |
28 | const defaultStops = {
29 | stops: [
30 | ['rgba(0, 0, 0, 1)', 0, 0],
31 | ['rgba(183, 80, 174, 0.92)', 1, 1]
32 | ],
33 | gradient: `linear-gradient(180deg, rgba(6, 6, 6, 1) 0.0%, rgba(183, 80, 174, 0.92) 100.0%)`,
34 | modifier: 180,
35 | type: 'linear'
36 | };
37 |
38 | if (str === 'transparent') {
39 | return defaultStops;
40 | }
41 |
42 | if (
43 | tinyColor.isValid() &&
44 | !str.trim().startsWith('radial-gradient') &&
45 | !str.trim().startsWith('linear-gradient')
46 | ) {
47 | const rgbaStr = tinyColor.toRgbString();
48 |
49 | if (rgbaStr) {
50 | defaultStops.stops = [
51 | ['rgba(0, 0, 0, 1)', 0, 0],
52 | [rgbaStr, 1, 1]
53 | ];
54 | defaultStops.gradient = `linear-gradient(180deg, rgba(6, 6, 6, 1) 0.0%, ${rgbaStr} 100.0%)`;
55 | }
56 |
57 | return defaultStops;
58 | } else {
59 | str = str.replace(';', '').replace('background-image:', '');
60 | const gradient = validGradient(str);
61 |
62 | let stops: Array = [];
63 | let angle: string = '';
64 |
65 | if (
66 | gradient === 'Failed to find gradient' ||
67 | gradient === 'Not correct position'
68 | ) {
69 | console.warn('Incorrect gradient value');
70 | return defaultStops;
71 | }
72 |
73 | if (typeof gradient !== 'string') {
74 | stops = gradient.stops;
75 | angle = gradient.angle ? gradient.angle : gradient.line;
76 | }
77 |
78 | const [, type, content] = str.match(/^(\w+)-gradient\((.*)\)$/i) || [];
79 | if (!type || !content) {
80 | console.warn('Incorrect gradient value');
81 | return defaultStops;
82 | }
83 |
84 | const findF = LINEAR_POS.find((item) => item.name === angle)?.angle;
85 | const helperAngle = type === 'linear' ? '180' : 'circle at center';
86 | const modifier = findF || angle || helperAngle;
87 |
88 | return {
89 | gradient: `${type}-gradient(${
90 | typeof gradient !== 'string' ? gradient.original : str
91 | })`,
92 | type,
93 | modifier:
94 | modifier.match(/\d+/) !== null
95 | ? Number(modifier.match(/\d+/)?.join(''))
96 | : modifier,
97 | stops: stops.map((stop, index: number) => {
98 | const formatStop = [`${stop.color}`, index];
99 | if (stop.position || stop.position === 0) {
100 | formatStop.splice(1, 0, stop.position);
101 | }
102 | return formatStop;
103 | })
104 | };
105 | }
106 | };
107 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/rgbaToArray.ts:
--------------------------------------------------------------------------------
1 | export default (color: any) => {
2 | if (!color) return;
3 | if (color.toLowerCase() === 'transparent') return [0, 0, 0, 0];
4 | if (color[0] === '#') {
5 | if (color.length < 7) {
6 | color =
7 | '#' +
8 | color[1] +
9 | color[1] +
10 | color[2] +
11 | color[2] +
12 | color[3] +
13 | color[3] +
14 | (color.length > 4 ? color[4] + color[4] : '');
15 | }
16 | return [
17 | parseInt(color.substr(1, 2), 16),
18 | parseInt(color.substr(3, 2), 16),
19 | parseInt(color.substr(5, 2), 16),
20 | color.length > 7 ? parseInt(color.substr(7, 2), 16) / 255 : 1
21 | ];
22 | }
23 |
24 | if (color.indexOf('rgb') === 0) {
25 | color += ',1';
26 | // eslint-disable-next-line
27 | return color.match(/[\.\d]+/g).map((a: string) => {
28 | return +a;
29 | });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/rgbaToHex.ts:
--------------------------------------------------------------------------------
1 | export default (params: Array) => {
2 | if (!Array.isArray(params)) return '';
3 |
4 | if (params.length < 3 || params.length > 4) return '';
5 |
6 | const parts = params.map(function (e: string | number) {
7 | let r = (+e).toString(16);
8 | r.length === 1 && (r = '0' + r);
9 | return r;
10 | }, []);
11 |
12 | return !~parts.indexOf('NaN') ? '#' + parts.join('') : '';
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/color-picker/utils/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export default (value: T, delay?: number): T => {
4 | const [debouncedValue, setDebouncedValue] = useState(value);
5 |
6 | useEffect(() => {
7 | const handler = setTimeout(() => {
8 | setDebouncedValue(value);
9 | }, delay);
10 | return () => {
11 | clearTimeout(handler);
12 | };
13 | }, [value, delay]);
14 |
15 | return debouncedValue;
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/featured-testimonials.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatedTooltip } from "@/components/ui/animated-tooltip";
2 | const people = [
3 | {
4 | id: 1,
5 | name: "John Doe",
6 | designation: "Software Engineer",
7 | image:
8 | "https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3387&q=80"
9 | },
10 | {
11 | id: 2,
12 | name: "Robert Johnson",
13 | designation: "Product Manager",
14 | image:
15 | "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60"
16 | },
17 | {
18 | id: 3,
19 | name: "Jane Smith",
20 | designation: "Data Scientist",
21 | image:
22 | "https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NXx8YXZhdGFyfGVufDB8fDB8fHww&auto=format&fit=crop&w=800&q=60"
23 | },
24 | {
25 | id: 4,
26 | name: "Emily Davis",
27 | designation: "UX Designer",
28 | image:
29 | "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTB8fGF2YXRhcnxlbnwwfHwwfHx8MA%3D%3D&auto=format&fit=crop&w=800&q=60"
30 | },
31 | {
32 | id: 5,
33 | name: "Tyler Durden",
34 | designation: "Soap Developer",
35 | image:
36 | "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3540&q=80"
37 | },
38 | {
39 | id: 6,
40 | name: "Dora",
41 | designation: "The Explorer",
42 | image:
43 | "https://images.unsplash.com/photo-1544725176-7c40e5a71c5e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=3534&q=80"
44 | }
45 | ];
46 |
47 | export function FeaturedTestimonials() {
48 | return (
49 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/horizontal-gradient.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { useId } from "react";
3 |
4 | export const HorizontalGradient = ({
5 | className,
6 | ...props
7 | }: {
8 | className: string;
9 | [x: string]: any;
10 | }) => {
11 | const id = useId();
12 | return (
13 |
25 |
30 |
31 |
32 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/password.tsx:
--------------------------------------------------------------------------------
1 | import { EyeIcon, EyeOffIcon } from "lucide-react";
2 | import React from "react";
3 | import { useState } from "react";
4 |
5 | import { Control, Path } from "react-hook-form";
6 | import { FieldValues } from "react-hook-form";
7 |
8 | export interface CommonReactHookFormProps {
9 | name: Path;
10 | control: Control;
11 | }
12 |
13 | interface PasswordProps
14 | extends React.InputHTMLAttributes<
15 | HTMLInputElement & CommonReactHookFormProps
16 | > {}
17 |
18 | function Password(props: PasswordProps) {
19 | const [show, setShow] = useState(false);
20 | return (
21 |
22 |
29 |
30 | {!show && (
31 | setShow(true)}
33 | className="text-gray-400 cursor-pointer h-4"
34 | />
35 | )}
36 | {show && (
37 | setShow(false)}
39 | className="text-gray-400 cursor-pointer h-4"
40 | />
41 | )}
42 |
43 |
44 | );
45 | }
46 |
47 | export default Password;
48 |
--------------------------------------------------------------------------------
/src/components/shared/draggable.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, cloneElement, ReactElement, useRef } from "react";
2 | import { createPortal } from "react-dom";
3 |
4 | interface DraggableProps {
5 | children: ReactElement;
6 | shouldDisplayPreview?: boolean;
7 | renderCustomPreview?: ReactElement;
8 | data?: Record;
9 | }
10 |
11 | const Draggable: React.FC = ({
12 | children,
13 | renderCustomPreview,
14 | data = {},
15 | shouldDisplayPreview = true
16 | }) => {
17 | const [isDragging, setIsDragging] = useState(false);
18 | const [position, setPosition] = useState({ x: 0, y: 0 });
19 | const previewRef = useRef(null);
20 | const handleDragStart = (e: React.DragEvent) => {
21 | setIsDragging(true);
22 | e.dataTransfer.setDragImage(new Image(), 0, 0); // Hides default preview
23 | // set drag data
24 | e.dataTransfer.setData("transition", JSON.stringify(data));
25 | setPosition({
26 | x: e.clientX,
27 | y: e.clientY
28 | });
29 | };
30 |
31 | const handleDragEnd = () => {
32 | setIsDragging(false);
33 | };
34 |
35 | const handleDrag = (e: React.DragEvent) => {
36 | if (isDragging) {
37 | setPosition({
38 | x: e.clientX,
39 | y: e.clientY
40 | });
41 | }
42 | };
43 |
44 | const childWithProps = cloneElement(children, {
45 | draggable: true,
46 | onDragStart: handleDragStart,
47 | onDragEnd: handleDragEnd,
48 | onDrag: handleDrag,
49 | style: {
50 | ...children.props.style,
51 | cursor: "grab"
52 | }
53 | });
54 |
55 | return (
56 | <>
57 | {childWithProps}
58 | {isDragging && shouldDisplayPreview && renderCustomPreview
59 | ? createPortal(
60 |
71 | {renderCustomPreview}
72 |
,
73 | document.body
74 | )
75 | : null}
76 | >
77 | );
78 | };
79 |
80 | export default Draggable;
81 |
--------------------------------------------------------------------------------
/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/animated-circular-progress.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | interface Props {
4 | max: number;
5 | value: number;
6 | min: number;
7 | gaugePrimaryColor: string;
8 | gaugeSecondaryColor: string;
9 | className?: string;
10 | }
11 |
12 | export default function AnimatedCircularProgressBar({
13 | max = 100,
14 | min = 0,
15 | value = 0,
16 | gaugePrimaryColor,
17 | gaugeSecondaryColor,
18 | className
19 | }: Props) {
20 | const circumference = 2 * Math.PI * 45;
21 | const percentPx = circumference / 100;
22 | const currentPercent = ((value - min) / (max - min)) * 100;
23 |
24 | return (
25 |
42 |
48 | {currentPercent <= 90 && currentPercent >= 0 && (
49 |
73 | )}
74 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/ui/animated-tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | motion,
4 | useTransform,
5 | AnimatePresence,
6 | useMotionValue,
7 | useSpring
8 | } from "framer-motion";
9 |
10 | export const AnimatedTooltip = ({
11 | items
12 | }: {
13 | items: {
14 | id: number;
15 | name: string;
16 | designation: string;
17 | image: string;
18 | }[];
19 | }) => {
20 | const [hoveredIndex, setHoveredIndex] = useState(null);
21 | const springConfig = { stiffness: 100, damping: 5 };
22 | const x = useMotionValue(0); // going to set this value on mouse move
23 | // rotate the tooltip
24 | const rotate = useSpring(
25 | useTransform(x, [-100, 100], [-45, 45]),
26 | springConfig
27 | );
28 | // translate the tooltip
29 | const translateX = useSpring(
30 | useTransform(x, [-100, 100], [-50, 50]),
31 | springConfig
32 | );
33 | const handleMouseMove = (event: any) => {
34 | const halfWidth = event.target.offsetWidth / 2;
35 | x.set(event.nativeEvent.offsetX - halfWidth); // set the x value, which is then used in transform and rotate
36 | };
37 |
38 | return (
39 | <>
40 | {items.map((item) => (
41 | setHoveredIndex(item.id)}
45 | onMouseLeave={() => setHoveredIndex(null)}
46 | >
47 |
48 | {hoveredIndex === item.id && (
49 |
69 |
70 |
71 |
72 | {item.name}
73 |
74 | {item.designation}
75 |
76 | )}
77 |
78 |
86 |
87 | ))}
88 | >
89 | );
90 | };
91 |
--------------------------------------------------------------------------------
/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 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
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/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "@/lib/utils";
3 |
4 | export interface InputProps
5 | extends React.InputHTMLAttributes {
6 | variant?: "default" | "secondary"; // Add variant prop
7 | }
8 |
9 | const Input = React.forwardRef(
10 | ({ className, type = "text", variant = "default", ...props }, ref) => {
11 | // Define base styles and variant styles
12 | const baseStyles =
13 | "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50";
14 | const variantStyles = {
15 | default: "",
16 | secondary:
17 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80"
18 | };
19 |
20 | return (
21 |
31 | );
32 | }
33 | );
34 |
35 | Input.displayName = "Input";
36 |
37 | export { Input };
38 |
--------------------------------------------------------------------------------
/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 PopoverAnchor = PopoverPrimitive.Anchor
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
32 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import { DragHandleDots2Icon } from "@radix-ui/react-icons"
2 | import * as ResizablePrimitive from "react-resizable-panels"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ResizablePanelGroup = ({
7 | className,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
17 | )
18 |
19 | const ResizablePanel = ResizablePrimitive.Panel
20 |
21 | const ResizableHandle = ({
22 | withHandle,
23 | className,
24 | ...props
25 | }: React.ComponentProps & {
26 | withHandle?: boolean
27 | }) => (
28 | div]:rotate-90",
31 | className
32 | )}
33 | {...props}
34 | >
35 | {withHandle && (
36 |
37 |
38 |
39 | )}
40 |
41 | )
42 |
43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
44 |
--------------------------------------------------------------------------------
/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/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 |
53 | {children}
54 |
55 | );
56 | });
57 |
58 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
59 |
60 | export { ToggleGroup, ToggleGroupItem };
61 |
--------------------------------------------------------------------------------
/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 transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring 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 shadow-sm hover:bg-accent hover:text-accent-foreground",
15 | secondary:
16 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80"
17 | },
18 | size: {
19 | default: "h-9 px-3",
20 | sm: "h-8 px-2",
21 | lg: "h-10 px-3"
22 | }
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default"
27 | }
28 | }
29 | );
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ));
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName;
44 |
45 | export { Toggle, toggleVariants };
46 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const GIANT_ICON_SIZE = 100;
2 | export const LARGE_ICON_SIZE = 30;
3 | export const NORMAL_ICON_SIZE = 18;
4 | export const SMALL_ICON_SIZE = 14;
5 |
6 | export const LARGER_FONT_SIZE = 30;
7 | export const LARGE_FONT_SIZE = 24;
8 | export const NORMAL_FONT_SIZE = 16;
9 | export const SMALL_FONT_SIZE = 12;
10 |
11 | export const DEFAULT_FONT = "Roboto";
12 | export const DEFAULT_WEIGHT = "Regular";
13 | export const SECONDARY_FONT = "sans-serif";
14 |
15 | export const PREVIEW_FRAME_WIDTH = 188;
16 | export const TIMELINE_OFFSET_X = 40;
17 |
18 | export const BASE_TIMELINE_ELEMENT_DURATION_MS = 4000;
19 |
20 | export const DEFAULT_VIDEO_WIDTH = 1920;
21 | export const DEFAULT_VIDEO_HEIGHT = 1080;
22 | export const DEFAULT_FRAMERATE = 60;
23 | export const FRAME_INTERVAL = 1000 / DEFAULT_FRAMERATE;
24 | export const DEFAULT_VIDEO_MIN_BITRATE = 2000000;
25 | export const DEFAULT_VIDEO_MAX_BITRATE = 10000000;
26 |
27 | export const DEFAULT_AUDIO_SAMPLE_RATE = 48000;
28 | export const DEFAULT_AUDIO_BITRATE = 192000;
29 |
30 | export const DEFAULT_PREVIEW_SCALE = 2.3;
31 |
32 | export const DEFAULT_PREVIEW_WIDTH =
33 | DEFAULT_VIDEO_WIDTH / DEFAULT_PREVIEW_SCALE;
34 |
35 | export const DEFAULT_PREVIEW_HEIGHT =
36 | DEFAULT_VIDEO_HEIGHT / DEFAULT_PREVIEW_SCALE;
37 |
38 | export const MIN_MEDIA_PANEL_WIDTH = 184;
39 | export const DEFAULT_MEDIA_PANEL_WIDTH = 348;
40 | export const MAX_MEDIA_PANEL_WIDTH = 512;
41 |
42 | export const DEFAULT_SETTINGS_PANEL_WIDTH = 300;
43 |
44 | export const DEFAULT_MIN_FADE = 0;
45 | export const DEFAULT_MAX_FADE = 5000;
46 | export const DEFAULT_FADE_STEP = 1;
47 | export const DEFAULT_FADE_IN = 0;
48 | export const DEFAULT_FADE_OUT = 0;
49 |
50 | export const DEFAULT_MIN_ROTATION = 0;
51 | export const DEFAULT_MAX_ROTATION = 359;
52 | export const DEFAULT_ROTATION_STEP = 1;
53 | export const DEFAULT_ROTATION = 0;
54 |
55 | export const DEFAULT_FLIP_X = false;
56 | export const DEFAULT_FLIP_Y = false;
57 |
58 | export const DEFAULT_MIN_BRIGHTNESS = 0;
59 | export const DEFAULT_MAX_BRIGHTNESS = 2;
60 | export const DEFAULT_BRIGHTNESS_STEP = 0.01;
61 | export const DEFAULT_BRIGHTNESS = 1;
62 |
63 | export const DEFAULT_MIN_SATURATION = 0;
64 | export const DEFAULT_MAX_SATURATION = 3;
65 | export const DEFAULT_SATURATION_STEP = 0.01;
66 | export const DEFAULT_SATURATION = 1;
67 |
68 | export const DEFAULT_MIN_TEMPERATURE = 0;
69 | export const DEFAULT_MAX_TEMPERATURE = 1;
70 | export const DEFAULT_TEMPERATURE_STEP = 0.01;
71 | export const DEFAULT_TEMPERATURE = 0.5;
72 |
73 | export const DEFAULT_MIN_CONTRAST = -1000;
74 | export const DEFAULT_MAX_CONTRAST = 1000;
75 | export const DEFAULT_CONTRAST_STEP = 1;
76 | export const DEFAULT_CONTRAST = 1;
77 |
78 | export const DEFAULT_MIN_OPACITY = 0;
79 | export const DEFAULT_MAX_OPACITY = 1;
80 | export const DEFAULT_OPACITY_STEP = 0.01;
81 | export const DEFAULT_OPACITY = 1;
82 |
83 | export const DEFAULT_MIN_BLUR = 0;
84 | export const DEFAULT_MAX_BLUR = 1;
85 | export const DEFAULT_BLUR_STEP = 0.01;
86 | export const DEFAULT_BLUR = 0;
87 |
88 | export const DEFAULT_MIN_SPEED = 0.5;
89 | export const DEFAULT_MAX_SPEED = 10;
90 | export const DEFAULT_SPEED_STEP = 0.01;
91 | export const DEFAULT_SPEED = 1;
92 |
93 | export const DEFAULT_MIN_VOLUME = 0;
94 | export const DEFAULT_MAX_VOLUME = 1;
95 | export const DEFAULT_VOLUME_STEP = 0.01;
96 | export const DEFAULT_VOLUME = 1;
97 |
--------------------------------------------------------------------------------
/src/constants/font.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_FONT = {
2 | id: "font_UwdNKSyVq2iiMiuHSRRsUIOu",
3 | family: "Roboto",
4 | fullName: "Roboto Bold",
5 | postScriptName: "Roboto-Bold",
6 | preview: "https://ik.imagekit.io/lh/fonts/v2/5zQgS86djScKA0ri67BBCqW7.png",
7 | style: "Roboto-Bold",
8 | url: "https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf",
9 | category: "sans-serif",
10 | createdAt: "2023-06-20T04:42:55.909Z",
11 | updatedAt: "2023-06-20T04:42:55.909Z",
12 | userId: null
13 | };
14 |
--------------------------------------------------------------------------------
/src/constants/scale.ts:
--------------------------------------------------------------------------------
1 | import { ITimelineScaleState } from "@designcombo/types";
2 |
3 | export const CURSOR_WIDTH = 12;
4 | export const CURSOR_CENTER = CURSOR_WIDTH / 2 - 2;
5 | export const TRACK_PADDING = 20;
6 |
7 | export const TIMELINE_ZOOM_LEVELS: ITimelineScaleState[] = [
8 | {
9 | // 1x distance (minute 0 to minute 5, 5 segments).
10 | unit: 18000,
11 | zoom: 1 / 12000,
12 | segments: 5
13 | },
14 | {
15 | // 1x distance (minute 0 to minute 3, 3 segments).
16 | unit: 10800,
17 | zoom: 1 / 7200,
18 | segments: 3
19 | },
20 | {
21 | // 1x distance (minute 0 to minute 2, 2 segments).
22 | unit: 7200,
23 | zoom: 1 / 6000,
24 | segments: 2
25 | },
26 | {
27 | // 1x distance (minute 0 to minute 1, 1 segment).
28 | unit: 3600,
29 | zoom: 1 / 3000,
30 | segments: 1
31 | },
32 | {
33 | // 1x distance (second 0 to second 30, 2 segments).
34 | unit: 1800,
35 | zoom: 1 / 1200,
36 | segments: 2
37 | },
38 | {
39 | // 1x distance (second 0 to second 15, 3 segments).
40 | unit: 900,
41 | zoom: 1 / 600,
42 | segments: 3
43 | },
44 | {
45 | // 1x distance (second 0 to second 10, 2 segments).
46 | unit: 600,
47 | zoom: 1 / 450,
48 | segments: 2
49 | },
50 | {
51 | // 1x distance (second 0 to second 5, 5 segments).
52 | unit: 300,
53 | zoom: 1 / 240,
54 | segments: 5
55 | },
56 | {
57 | // 1x distance (second 0 to second 3, 3 segments).
58 | unit: 180,
59 | zoom: 1 / 150,
60 | segments: 3
61 | },
62 | {
63 | // 1x distance (second 0 to second 2, 2 segments).
64 | unit: 120,
65 | zoom: 1 / 120,
66 | segments: 10
67 | },
68 | {
69 | // 1x distance (second 0 to second 1, 1 segment).
70 | unit: 60,
71 | zoom: 1 / 90,
72 | segments: 5
73 | },
74 |
75 | {
76 | // 1x distance (second 0 to second 1, 1 segment).
77 | unit: 60,
78 | zoom: 1 / 60,
79 | segments: 5
80 | },
81 | {
82 | // 1x distance (frame 0 to frame 30, 2 segments).
83 | unit: 30,
84 | zoom: 1 / 30,
85 | segments: 2
86 | },
87 | {
88 | // 1x distance (frame 0 to frame 15, 3 segments).
89 | unit: 15,
90 | zoom: 1 / 15,
91 | segments: 3
92 | },
93 | {
94 | // 1x distance (frame 0 to frame 10, 2 segments).
95 | unit: 10,
96 | zoom: 1 / 10,
97 | segments: 2
98 | },
99 | {
100 | // 1x distance (frame 0 to frame 5, 5 segments).
101 | unit: 5,
102 | zoom: 1 / 5,
103 | segments: 5
104 | },
105 | {
106 | // 1x distance (frame 0 to frame 3, 3 segments).
107 | unit: 3,
108 | zoom: 1 / 3,
109 | segments: 3
110 | },
111 | {
112 | // 1x distance (frame 0 to frame 2, 2 segments).
113 | unit: 2,
114 | zoom: 1 / 2,
115 | segments: 5
116 | },
117 | {
118 | // 1x distance (frame 0 to frame 1, 1 segment).
119 | unit: 1,
120 | zoom: 1,
121 | segments: 5
122 | },
123 | {
124 | // 2x distance (frame 0 to frame 1, 1 segment).
125 | unit: 1,
126 | zoom: 3,
127 | segments: 5
128 | },
129 | {
130 | // 4x distance (frame 0 to frame 1, 1 segment).
131 | unit: 1,
132 | zoom: 4,
133 | segments: 10
134 | }
135 | ];
136 |
--------------------------------------------------------------------------------
/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://ik.imagekit.io/wombo/images/img1.jpg"
5 | },
6 | {
7 | id: 2,
8 | src: "https://ik.imagekit.io/wombo/images/img2.jpg"
9 | },
10 | {
11 | id: 3,
12 | src: "https://ik.imagekit.io/wombo/images/img3.jpg"
13 | },
14 |
15 | {
16 | id: 4,
17 | src: "https://ik.imagekit.io/wombo/images/img4.jpg"
18 | },
19 | {
20 | id: 5,
21 | src: "https://ik.imagekit.io/wombo/images/img5.jpg"
22 | },
23 | ,
24 | {
25 | id: 6,
26 | src: "https://ik.imagekit.io/wombo/images/img6.jpg"
27 | },
28 | {
29 | id: 7,
30 | src: "https://ik.imagekit.io/wombo/images/img7.jpg"
31 | }
32 | ] as { id: number; src: string }[];
33 |
--------------------------------------------------------------------------------
/src/data/transitions.ts:
--------------------------------------------------------------------------------
1 | // from iTransition interface, omit fromId, toId
2 | export const TRANSITIONS: Omit[] = [
3 | {
4 | id: "1",
5 | type: "none",
6 | duration: 0,
7 | preview: "https://ik.imagekit.io/wombo/transitions-v2/transition-none.png"
8 | },
9 | {
10 | id: "2",
11 | type: "fade",
12 | duration: 0.5,
13 | preview: "https://ik.imagekit.io/wombo/transitions-v2/fade.webp"
14 | },
15 | {
16 | id: "3",
17 | type: "slide",
18 | name: "slide up",
19 | duration: 0.5,
20 | preview: "https://ik.imagekit.io/wombo/transitions-v2/slide-up.webp",
21 | direction: "from-bottom"
22 | },
23 | {
24 | id: "4",
25 | type: "slide",
26 | name: "slide down",
27 | duration: 0.5,
28 | preview: "https://ik.imagekit.io/wombo/transitions-v2/slide-down.webp",
29 | direction: "from-top"
30 | },
31 | {
32 | id: "5",
33 | type: "slide",
34 | name: "slide left",
35 | duration: 0.5,
36 | preview: "https://ik.imagekit.io/wombo/transitions-v2/slide-left.webp",
37 | direction: "from-right"
38 | },
39 | {
40 | id: "6",
41 | type: "slide",
42 | name: "slide right",
43 | duration: 0.5,
44 | preview: "https://ik.imagekit.io/wombo/transitions-v2/slide-right.webp",
45 | direction: "from-left"
46 | },
47 | {
48 | id: "7",
49 | type: "wipe",
50 | name: "wipe up",
51 | duration: 0.5,
52 | preview: "https://ik.imagekit.io/wombo/transitions-v2/wipe-up.webp",
53 | direction: "from-bottom"
54 | },
55 | {
56 | id: "8",
57 | type: "wipe",
58 | name: "wipe down",
59 | duration: 0.5,
60 | preview: "https://ik.imagekit.io/wombo/transitions-v2/wipe-down.webp",
61 | direction: "from-top"
62 | },
63 | {
64 | id: "9",
65 | type: "wipe",
66 | name: "wipe left",
67 | duration: 0.5,
68 | preview: "https://ik.imagekit.io/wombo/transitions-v2/wipe-left.webp",
69 | direction: "from-right"
70 | },
71 | {
72 | id: "10",
73 | type: "wipe",
74 | name: "wipe right",
75 | duration: 0.5,
76 | preview: "https://ik.imagekit.io/wombo/transitions-v2/wipe-right.webp",
77 | direction: "from-left"
78 | },
79 | {
80 | id: "11",
81 | type: "flip",
82 | duration: 0.5,
83 | preview: "https://ik.imagekit.io/wombo/transitions-v2/flip.webp"
84 | },
85 | {
86 | id: "12",
87 | type: "clockWipe",
88 | duration: 0.5,
89 | preview: "https://ik.imagekit.io/wombo/transitions-v2/clock-wipe.webp"
90 | },
91 | {
92 | id: "13",
93 | type: "star",
94 | duration: 0.5,
95 | preview: "https://ik.imagekit.io/wombo/transitions-v2/star.webp"
96 | },
97 | {
98 | id: "14",
99 | type: "circle",
100 | duration: 0.5,
101 | preview: "https://ik.imagekit.io/wombo/transitions-v2/circle.webp"
102 | },
103 | {
104 | id: "15",
105 | type: "rectangle",
106 | duration: 0.5,
107 | preview: "https://ik.imagekit.io/wombo/transitions-v2/rectangle.webp"
108 | }
109 | ];
110 |
--------------------------------------------------------------------------------
/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,https://cdn.designcombo.dev/videos/demo-video-1.mp4
4 | src: "https://cdn.designcombo.dev/videos/demo-video-1.mp4",
5 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-1.png"
6 | },
7 | {
8 | id: 2,
9 | src: "https://cdn.designcombo.dev/videos/demo-video-2.mp4",
10 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-2.png"
11 | },
12 | {
13 | id: 3,
14 | src: "https://cdn.designcombo.dev/videos/demo-video-3.mp4",
15 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-3.png"
16 | },
17 | {
18 | id: 4,
19 | src: "https://cdn.designcombo.dev/videos/demo-video-4.mp4",
20 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-4.png"
21 | },
22 | {
23 | id: 5,
24 | src: "https://cdn.designcombo.dev/videos/demo-video-5.mp4",
25 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-5.png"
26 | },
27 | {
28 | id: 6,
29 | src: "https://cdn.designcombo.dev/videos/demo-video-6.mp4",
30 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-6.png"
31 | },
32 | {
33 | id: 7,
34 | src: "https://cdn.designcombo.dev/videos/demo-video-7.mp4",
35 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-7.png"
36 | },
37 | {
38 | id: 8,
39 | src: "https://cdn.designcombo.dev/videos/demo-video-8.mp4",
40 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-8.png"
41 | },
42 | {
43 | id: 9,
44 | src: "https://cdn.designcombo.dev/videos/demo-video-9.mp4",
45 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-9.png"
46 | },
47 | {
48 | id: 10,
49 | src: "https://cdn.designcombo.dev/videos/demo-video-10.mp4",
50 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-10.png"
51 | },
52 | {
53 | id: 11,
54 | src: "https://cdn.designcombo.dev/videos/demo-video-11.mp4",
55 | preview: "https://cdn.designcombo.dev/thumbnails/demo-video-s-11.png"
56 | }
57 | ];
58 |
--------------------------------------------------------------------------------
/src/hooks/use-current-frame.tsx:
--------------------------------------------------------------------------------
1 | import { CallbackListener, PlayerRef } from "@remotion/player";
2 | import { useCallback, useSyncExternalStore } from "react";
3 |
4 | export const useCurrentPlayerFrame = (ref: React.RefObject) => {
5 | const subscribe = useCallback(
6 | (onStoreChange: () => void) => {
7 | const { current } = ref;
8 | if (!current) {
9 | return () => undefined;
10 | }
11 | const updater: CallbackListener<"frameupdate"> = () => {
12 | onStoreChange();
13 | };
14 | current.addEventListener("frameupdate", updater);
15 | return () => {
16 | current.removeEventListener("frameupdate", updater);
17 | };
18 | },
19 | [ref]
20 | );
21 | const data = useSyncExternalStore(
22 | subscribe,
23 | () => ref.current?.getCurrentFrame() ?? 0,
24 | () => 0
25 | );
26 | return data;
27 | };
28 |
--------------------------------------------------------------------------------
/src/hooks/use-scroll-top.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const useScrollTop = (threshold = 10) => {
4 | const [scrolled, setScrolled] = useState(false);
5 |
6 | useEffect(() => {
7 | const handleScroll = () => {
8 | if (window.scrollY > threshold) {
9 | setScrolled(true);
10 | } else {
11 | setScrolled(false);
12 | }
13 | };
14 | window.addEventListener("scroll", handleScroll);
15 |
16 | return () => {
17 | window.removeEventListener("scroll", handleScroll);
18 | };
19 | }, [threshold]);
20 | return scrolled;
21 | };
22 |
23 | export default useScrollTop;
24 |
--------------------------------------------------------------------------------
/src/hooks/use-timeline-events.ts:
--------------------------------------------------------------------------------
1 | import useStore from "@/store/store";
2 | import { useEffect } from "react";
3 | import {
4 | LAYER_PREFIX,
5 | LAYER_SELECTION,
6 | PLAYER_PAUSE,
7 | PLAYER_PLAY,
8 | PLAYER_PREFIX,
9 | PLAYER_SEEK,
10 | PLAYER_SEEK_BY,
11 | PLAYER_TOGGLE_PLAY,
12 | filter,
13 | subject
14 | } from "@designcombo/events";
15 | const useTimelineEvents = () => {
16 | const { playerRef, fps, timeline, setState } = useStore();
17 |
18 | //handle player events
19 | useEffect(() => {
20 | const playerEvents = subject.pipe(
21 | filter(({ key }) => key.startsWith(PLAYER_PREFIX))
22 | );
23 |
24 | const playerEventsSubscription = playerEvents.subscribe((obj) => {
25 | if (obj.key === PLAYER_SEEK) {
26 | const { time } = obj.value?.payload;
27 | playerRef?.current?.seekTo((time / 1000) * fps);
28 | } else if (obj.key === PLAYER_PLAY) {
29 | playerRef?.current?.play();
30 | } else if (obj.key === PLAYER_PAUSE) {
31 | playerRef?.current?.pause();
32 | } else if (obj.key === PLAYER_TOGGLE_PLAY) {
33 | if (playerRef?.current?.isPlaying()) {
34 | playerRef?.current?.pause();
35 | } else {
36 | playerRef?.current?.play();
37 | }
38 | } else if (obj.key === PLAYER_SEEK_BY) {
39 | const { frames } = obj.value?.payload;
40 | playerRef?.current?.seekTo(
41 | Math.round(playerRef?.current?.getCurrentFrame()) + frames
42 | );
43 | }
44 | });
45 |
46 | return () => playerEventsSubscription.unsubscribe();
47 | }, [playerRef, fps]);
48 |
49 | // handle selection events
50 | useEffect(() => {
51 | const selectionEvents = subject.pipe(
52 | filter(({ key }) => key.startsWith(LAYER_PREFIX))
53 | );
54 |
55 | const selectionSubscription = selectionEvents.subscribe((obj) => {
56 | if (obj.key === LAYER_SELECTION) {
57 | setState({
58 | activeIds: obj.value?.payload.activeIds
59 | });
60 | }
61 | });
62 | return () => selectionSubscription.unsubscribe();
63 | }, [timeline]);
64 | };
65 |
66 | export default useTimelineEvents;
67 |
--------------------------------------------------------------------------------
/src/interfaces/captions.ts:
--------------------------------------------------------------------------------
1 | export interface Word {
2 | end: number;
3 | start: number;
4 | word: string;
5 | }
6 | export interface CaptionsSegment {
7 | start: number;
8 | end: number;
9 | text: string;
10 | words: Word[];
11 | }
12 | export interface CaptionsData {
13 | segments: CaptionsSegment[];
14 | }
15 |
--------------------------------------------------------------------------------
/src/interfaces/editor.ts:
--------------------------------------------------------------------------------
1 | export interface IUpload {
2 | id: string;
3 | name: string;
4 | originalName: string;
5 | fileId: string;
6 | userId?: string;
7 | previewUrl: string;
8 | url: string;
9 | previewData?: string;
10 | }
11 | export interface User {
12 | id: string;
13 | email: string;
14 | avatar: string;
15 | username: string;
16 | provider: "github";
17 | }
18 | export interface IFont {
19 | id: string;
20 | family: string;
21 | fullName: string;
22 | postScriptName: string;
23 | preview: string;
24 | style: string;
25 | url: string;
26 | category: string;
27 | createdAt: string;
28 | updatedAt: string;
29 | userId: string | null;
30 | }
31 |
32 | export interface ICompactFont {
33 | family: string;
34 | styles: IFont[];
35 | default: IFont;
36 | name?: string;
37 | }
38 |
39 | export interface IDataState {
40 | fonts: IFont[];
41 | compactFonts: ICompactFont[];
42 | setFonts: (fonts: IFont[]) => void;
43 | setCompactFonts: (compactFonts: ICompactFont[]) => void;
44 | }
45 |
46 | export type IPropertyType = "textContent" | "fontSize" | "color";
47 |
48 | /**
49 | * Width / height
50 | */
51 | export type Ratio = number;
52 |
53 | export type Area = [x: number, y: number, width: number, height: number];
54 |
--------------------------------------------------------------------------------
/src/interfaces/layout.ts:
--------------------------------------------------------------------------------
1 | import { ITrackItem } from "@designcombo/types";
2 |
3 | export type IMenuItem =
4 | | "uploads"
5 | | "templates"
6 | | "videos"
7 | | "images"
8 | | "shapes"
9 | | "audios"
10 | | "transitions"
11 | | "texts"
12 | | "captions";
13 | export interface ILayoutState {
14 | cropTarget: ITrackItem | null;
15 | activeMenuItem: IMenuItem | null;
16 | showMenuItem: boolean;
17 | showControlItem: boolean;
18 | showToolboxItem: boolean;
19 | activeToolboxItem: string | null;
20 | setCropTarget: (cropTarget: ITrackItem | null) => void;
21 | setActiveMenuItem: (showMenu: IMenuItem | null) => void;
22 | setShowMenuItem: (showMenuItem: boolean) => void;
23 | setShowControlItem: (showControlItem: boolean) => void;
24 | setShowToolboxItem: (showToolboxItem: boolean) => void;
25 | setActiveToolboxItem: (activeToolboxItem: string | null) => void;
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } 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 { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import { ThemeProvider } from "@/components/theme-provider";
4 | import { createBrowserRouter, RouterProvider } from "react-router-dom";
5 | import "non.geist";
6 | import "./index.css";
7 | import App from "./app";
8 | import Auth from "./pages/auth";
9 |
10 | const router = createBrowserRouter([
11 | {
12 | path: "/",
13 | element: ,
14 | },
15 | {
16 | path: "/auth",
17 | element: ,
18 | },
19 | ]);
20 | createRoot(document.getElementById("root")!).render(
21 |
22 |
23 |
24 |
25 |
26 | );
27 |
--------------------------------------------------------------------------------
/src/pages/auth/auth-layout.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { HorizontalGradient } from "@/components/horizontal-gradient";
3 | import { FeaturedTestimonials } from "@/components/featured-testimonials";
4 |
5 | export function AuthLayout({ children }: { children: React.ReactNode }) {
6 | return (
7 | <>
8 |
9 | {children}
10 |
11 |
12 |
13 |
18 | Join thousands of users already
19 |
20 |
25 | With lots of AI applications around, Desigcombo stands out with
26 | its state of the art Shitposting capabilities.
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | >
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/auth/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./auth";
2 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/animations.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
2 |
3 | const Animations = () => {
4 | return (
5 |
6 |
7 | Animations
8 |
9 |
10 |
11 |
12 | In
13 | Out
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default Animations;
24 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/basic-audio.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollArea } from "@/components/ui/scroll-area";
2 | import { IAudio, ITrackItem } from "@designcombo/types";
3 | import Volume from "./common/volume";
4 | import Speed from "./common/speed";
5 | import { useState } from "react";
6 | import { EDIT_OBJECT, dispatch } from "@designcombo/events";
7 |
8 | const BasicAudio = ({ trackItem }: { trackItem: ITrackItem & IAudio }) => {
9 | const [properties, setProperties] = useState(trackItem);
10 |
11 | const handleChangeVolume = (v: number) => {
12 | dispatch(EDIT_OBJECT, {
13 | payload: {
14 | [trackItem.id]: {
15 | details: {
16 | volume: v
17 | }
18 | }
19 | }
20 | });
21 |
22 | setProperties((prev) => {
23 | return {
24 | ...prev,
25 | details: {
26 | ...prev.details,
27 | volume: v
28 | }
29 | };
30 | });
31 | };
32 |
33 | const handleChangeSpeed = (v: number) => {
34 | dispatch(EDIT_OBJECT, {
35 | payload: {
36 | [trackItem.id]: {
37 | playbackRate: v
38 | }
39 | }
40 | });
41 |
42 | setProperties((prev) => {
43 | return {
44 | ...prev,
45 | playbackRate: v
46 | };
47 | });
48 | };
49 |
50 | return (
51 |
52 |
53 | Audio
54 |
55 |
56 |
57 | handleChangeVolume(v)}
59 | value={properties.details.volume!}
60 | />
61 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default BasicAudio;
72 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import { Label } from "@/components/ui/label";
2 | import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
3 | import { useState } from "react";
4 |
5 | export default function AspectRatio() {
6 | const [value, setValue] = useState("locked");
7 | const onChangeAligment = (value: string) => {
8 | setValue(value);
9 | };
10 | return (
11 |
12 |
13 | Aspect ratio
14 |
15 |
16 |
24 |
25 | Locked
26 |
27 |
28 | Unlocked
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/blur.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 | import { Slider } from "@/components/ui/slider";
4 | import { useState, useEffect } from "react";
5 |
6 | const Blur = ({
7 | value,
8 | onChange
9 | }: {
10 | value: number;
11 | onChange: (v: number) => void;
12 | }) => {
13 | // Create local state to manage opacity
14 | const [localValue, setLocalValue] = useState(value);
15 |
16 | // Update local state when prop value changes
17 | useEffect(() => {
18 | setLocalValue(value);
19 | }, [value]);
20 |
21 | return (
22 |
23 |
24 | Blur
25 |
26 |
33 | {
37 | setLocalValue(e[0]); // Update local state
38 | }}
39 | onValueCommit={() => {
40 | onChange(localValue); // Propagate value to parent when user commits change
41 | }}
42 | min={0}
43 | max={100}
44 | step={1}
45 | aria-label="Blur"
46 | />
47 | {
53 | const newValue = Number(e.target.value);
54 | if (newValue >= 0 && newValue <= 100) {
55 | setLocalValue(newValue); // Update local state
56 | onChange(newValue); // Optionally propagate immediately, or adjust as needed
57 | }
58 | }}
59 | value={localValue} // Use local state for input value
60 | />
61 |
62 |
63 | );
64 | };
65 |
66 | export default Blur;
67 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/brightness.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 | import { Slider } from "@/components/ui/slider";
4 | import { useState, useEffect } from "react";
5 |
6 | const Brightness = ({
7 | value,
8 | onChange
9 | }: {
10 | value: number;
11 | onChange: (v: number) => void;
12 | }) => {
13 | // Create local state to manage opacity
14 | const [localValue, setLocalValue] = useState(value);
15 |
16 | // Update local state when prop value changes
17 | useEffect(() => {
18 | setLocalValue(value);
19 | }, [value]);
20 |
21 | return (
22 |
23 |
24 | Brightness
25 |
26 |
33 | {
37 | setLocalValue(e[0]); // Update local state
38 | }}
39 | onValueCommit={() => {
40 | onChange(localValue); // Propagate value to parent when user commits change
41 | }}
42 | min={0}
43 | max={100}
44 | step={1}
45 | aria-label="Brightness"
46 | />
47 | {
53 | const newValue = Number(e.target.value);
54 | if (newValue >= 0 && newValue <= 100) {
55 | setLocalValue(newValue); // Update local state
56 | onChange(newValue); // Optionally propagate immediately, or adjust as needed
57 | }
58 | }}
59 | value={localValue} // Use local state for input value
60 | />
61 |
62 |
63 | );
64 | };
65 |
66 | export default Brightness;
67 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/flip.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/button";
2 | import { Label } from "@/components/ui/label";
3 | import { EDIT_OBJECT, dispatch } from "@designcombo/events";
4 | import { IImage, ITrackItem, IVideo } from "@designcombo/types";
5 | import { useState } from "react";
6 |
7 | export default function Flip({
8 | trackItem
9 | }: {
10 | trackItem: ITrackItem & (IImage | IVideo);
11 | }) {
12 | const [flip, setFlip] = useState({
13 | flipX: trackItem.details.flipX,
14 | flipY: trackItem.details.flipY
15 | });
16 |
17 | const handleFlip = (value: string) => {
18 | if (value === "x") {
19 | dispatch(EDIT_OBJECT, {
20 | payload: {
21 | [trackItem.id]: {
22 | details: {
23 | flipX: !flip.flipX
24 | }
25 | }
26 | }
27 | });
28 | setFlip({ ...flip, flipX: !flip.flipX });
29 | } else if (value === "y") {
30 | dispatch(EDIT_OBJECT, {
31 | payload: {
32 | [trackItem.id]: {
33 | details: {
34 | flipY: !flip.flipY
35 | }
36 | }
37 | }
38 | });
39 | setFlip({ ...flip, flipY: !flip.flipY });
40 | }
41 | };
42 | return (
43 |
44 |
45 | Flip
46 |
47 |
48 | handleFlip("x")}>
49 | Flip X
50 |
51 | handleFlip("y")}>
52 | Flip Y
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/opacity.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 | import { Slider } from "@/components/ui/slider";
4 | import { useState, useEffect } from "react";
5 |
6 | const Opacity = ({
7 | value,
8 | onChange
9 | }: {
10 | value: number;
11 | onChange: (v: number) => void;
12 | }) => {
13 | // Create local state to manage opacity
14 | const [localValue, setLocalValue] = useState(value);
15 |
16 | // Update local state when prop value changes
17 | useEffect(() => {
18 | setLocalValue(value);
19 | }, [value]);
20 |
21 | return (
22 |
23 |
24 | Opacity
25 |
26 |
33 | {
37 | setLocalValue(e[0]); // Update local state
38 | }}
39 | onValueCommit={() => {
40 | onChange(localValue); // Propagate value to parent when user commits change
41 | }}
42 | min={0}
43 | max={100}
44 | step={1}
45 | aria-label="Opacity"
46 | />
47 | {
53 | const newValue = Number(e.target.value);
54 | if (newValue >= 0 && newValue <= 100) {
55 | setLocalValue(newValue); // Update local state
56 | onChange(newValue); // Optionally propagate immediately, or adjust as needed
57 | }
58 | }}
59 | value={localValue} // Use local state for input value
60 | />
61 |
62 |
63 | );
64 | };
65 |
66 | export default Opacity;
67 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/outline.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 | import { useEffect, useState } from "react";
4 | import {
5 | Popover,
6 | PopoverContent,
7 | PopoverTrigger
8 | } from "@/components/ui/popover";
9 | import ColorPicker from "@/components/color-picker";
10 | function Outline({
11 | label,
12 | onChageBorderWidth,
13 | onChangeBorderColor,
14 | valueBorderWidth,
15 | valueBorderColor
16 | }: {
17 | label: string;
18 | onChageBorderWidth: (v: number) => void;
19 | onChangeBorderColor: (v: string) => void;
20 | valueBorderWidth: number;
21 | valueBorderColor: string;
22 | }) {
23 | const [localValueBorderWidth, setLocalValueBorderWidth] = useState<
24 | string | number
25 | >(valueBorderWidth);
26 | const [localValueBorderColor, setLocalValueBorderColor] =
27 | useState(valueBorderColor); // Allow for string
28 |
29 | useEffect(() => {
30 | setLocalValueBorderWidth(valueBorderWidth);
31 | setLocalValueBorderColor(valueBorderColor);
32 | }, [valueBorderWidth, valueBorderColor]);
33 |
34 | return (
35 |
36 |
37 | {label}
38 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 | {
57 | setLocalValueBorderColor(v);
58 | onChangeBorderColor(v);
59 | }}
60 | allowAddGradientStops={true}
61 | />
62 |
63 |
64 |
65 |
66 |
{
70 | const newValue = e.target.value;
71 | setLocalValueBorderColor(newValue); // Update local state
72 |
73 | // Only propagate if it's not empty
74 | if (newValue !== "") {
75 | onChangeBorderColor(newValue); // Propagate the value
76 | }
77 | }}
78 | value={localValueBorderColor} // Use local state for input value
79 | />
80 |
81 | hex
82 |
83 |
84 |
85 |
86 |
{
91 | const newValue = e.target.value;
92 |
93 | // Allow empty string or validate as a number
94 | if (
95 | newValue === "" ||
96 | (!isNaN(Number(newValue)) &&
97 | Number(newValue) >= 0 &&
98 | Number(newValue) <= 100)
99 | ) {
100 | setLocalValueBorderWidth(newValue); // Update local state
101 |
102 | // Only propagate if it's a valid number and not empty
103 | if (newValue !== "") {
104 | onChageBorderWidth(Number(newValue)); // Propagate as a number
105 | }
106 | }
107 | }}
108 | value={localValueBorderWidth} // Use local state for input value
109 | />
110 |
111 | thickness
112 |
113 |
114 |
115 |
116 | );
117 | }
118 |
119 | export default Outline;
120 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/playback-rate.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/button";
2 | import { Label } from "@/components/ui/label";
3 | import { EDIT_OBJECT, dispatch } from "@designcombo/events";
4 | import { ITrackItem } from "@designcombo/types";
5 |
6 | export default function PlaybackRate({ trackItem }: { trackItem: ITrackItem }) {
7 | const handleChangePlaybackRate = (value: number) => {
8 | dispatch(EDIT_OBJECT, {
9 | payload: {
10 | [trackItem.id]: {
11 | playbackRate: value
12 | }
13 | }
14 | });
15 | };
16 | return (
17 |
18 |
19 | Aspect ratio
20 |
21 |
22 | {
25 | handleChangePlaybackRate(0.5);
26 | }}
27 | >
28 | x0.5
29 |
30 | {
33 | handleChangePlaybackRate(1);
34 | }}
35 | >
36 | x1
37 |
38 | {
41 | handleChangePlaybackRate(1.5);
42 | }}
43 | >
44 | x1.5
45 |
46 | {
49 | handleChangePlaybackRate(2);
50 | }}
51 | >
52 | x2
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/radius.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 |
4 | import { Slider } from "@/components/ui/slider";
5 | import { useEffect, useState } from "react";
6 |
7 | const Rounded = ({
8 | value,
9 | onChange
10 | }: {
11 | value: number;
12 | onChange: (v: number) => void;
13 | }) => {
14 | // Create local state to manage opacity
15 | const [localValue, setLocalValue] = useState(value);
16 |
17 | // Update local state when prop value changes
18 | useEffect(() => {
19 | setLocalValue(value);
20 | }, [value]);
21 |
22 | return (
23 |
24 |
25 | Rounded
26 |
27 |
34 | {
38 | setLocalValue(e[0]); // Update local state
39 | }}
40 | onValueCommit={() => {
41 | onChange(localValue); // Propagate value to parent when user commits change
42 | }}
43 | min={0}
44 | max={50}
45 | step={1}
46 | aria-label="rounded"
47 | />
48 | {
53 | const newValue = Number(e.target.value);
54 | if (newValue >= 0 && newValue <= 100) {
55 | setLocalValue(newValue); // Update local state
56 | onChange(newValue); // Optionally propagate immediately, or adjust as needed
57 | }
58 | }}
59 | value={localValue} // Use local state for input value
60 | />
61 |
62 |
63 | );
64 | };
65 |
66 | export default Rounded;
67 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/speed.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 |
4 | import { Slider } from "@/components/ui/slider";
5 | import { useEffect, useState } from "react";
6 |
7 | const Speed = ({
8 | value,
9 | onChange
10 | }: {
11 | value: number;
12 | onChange: (v: number) => void;
13 | }) => {
14 | // Create local state to manage opacity
15 | const [localValue, setLocalValue] = useState(value);
16 |
17 | // Update local state when prop value changes
18 | useEffect(() => {
19 | setLocalValue(value);
20 | }, [value]);
21 |
22 | return (
23 |
24 |
25 | Speed
26 |
27 |
34 | {
38 | setLocalValue(e[0]); // Update local state
39 | }}
40 | onValueCommit={() => {
41 | onChange(localValue); // Propagate value to parent when user commits change
42 | }}
43 | min={0}
44 | max={4}
45 | step={0.1}
46 | aria-label="Opacity"
47 | />
48 | {
54 | const newValue = Number(e.target.value);
55 | if (newValue >= 0 && newValue <= 4) {
56 | setLocalValue(newValue); // Update local state
57 | onChange(newValue); // Optionally propagate immediately, or adjust as needed
58 | }
59 | }}
60 | value={localValue} // Use local state for input value
61 | />
62 |
63 |
64 | );
65 | };
66 |
67 | export default Speed;
68 |
--------------------------------------------------------------------------------
/src/pages/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 [_, setValue] = useState([10]);
9 |
10 | return (
11 |
12 |
Transform
13 |
14 |
Scale
15 |
22 |
29 |
30 |
31 |
36 |
37 |
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 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
Rotate
78 |
85 |
86 |
87 |
88 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default Transform;
103 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/common/volume.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 |
4 | import { Slider } from "@/components/ui/slider";
5 | import { useEffect, useState } from "react";
6 |
7 | const Volume = ({
8 | value,
9 | onChange
10 | }: {
11 | value: number;
12 | onChange: (v: number) => void;
13 | }) => {
14 | // Create local state to manage opacity
15 | const [localValue, setLocalValue] = useState(value);
16 |
17 | // Update local state when prop value changes
18 | useEffect(() => {
19 | setLocalValue(value);
20 | }, [value]);
21 |
22 | return (
23 |
24 |
25 | Volume
26 |
27 |
34 | {
38 | setLocalValue(e[0]); // Update local state
39 | }}
40 | onValueCommit={() => {
41 | onChange(localValue); // Propagate value to parent when user commits change
42 | }}
43 | max={100}
44 | step={1}
45 | aria-label="Temperature"
46 | />
47 | {
52 | const newValue = Number(e.target.value);
53 | if (newValue >= 0 && newValue <= 100) {
54 | setLocalValue(newValue); // Update local state
55 | onChange(newValue); // Optionally propagate immediately, or adjust as needed
56 | }
57 | }}
58 | value={localValue} // Use local state for input value
59 | />
60 |
61 |
62 | );
63 | };
64 |
65 | export default Volume;
66 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/control-item.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useLayoutStore from "@/store/use-layout-store";
3 | import {
4 | IAudio,
5 | IImage,
6 | IText,
7 | ITrackItem,
8 | ITrackItemAndDetails,
9 | IVideo
10 | } from "@designcombo/types";
11 | import { useEffect, useState } from "react";
12 | import { Button } from "@/components/ui/button";
13 | import { X } from "lucide-react";
14 | import Presets from "./presets";
15 | import Animations from "./animations";
16 | import Smart from "./smart";
17 | import BasicText from "./basic-text";
18 | import BasicImage from "./basic-image";
19 | import BasicVideo from "./basic-video";
20 | import BasicAudio from "./basic-audio";
21 | import useStore from "@/store/store";
22 |
23 | const Container = ({ children }: { children: React.ReactNode }) => {
24 | const { activeToolboxItem, setActiveToolboxItem } = useLayoutStore();
25 | const { activeIds, trackItemsMap, trackItemDetailsMap } = useStore();
26 | const [trackItem, setTrackItem] = useState(null);
27 | const [displayToolbox, setDisplayToolbox] = useState(false);
28 |
29 | useEffect(() => {
30 | if (activeIds.length === 1) {
31 | const [id] = activeIds;
32 | const trackItemDetails = trackItemDetailsMap[id];
33 | const trackItem = {
34 | ...trackItemsMap[id],
35 | details: trackItemDetails?.details || {}
36 | };
37 | setTrackItem(trackItem);
38 | } else {
39 | setTrackItem(null);
40 | setDisplayToolbox(false);
41 | }
42 | }, [activeIds, trackItemsMap]);
43 |
44 | useEffect(() => {
45 | if (activeToolboxItem) {
46 | setDisplayToolbox(true);
47 | } else {
48 | setDisplayToolbox(false);
49 | }
50 | }, [activeToolboxItem]);
51 |
52 | if (!trackItem) {
53 | return null;
54 | }
55 |
56 | return (
57 |
65 |
66 |
71 | {
74 | setDisplayToolbox(false);
75 | setActiveToolboxItem(null);
76 | }}
77 | />
78 |
79 | {React.cloneElement(children as React.ReactElement
, {
80 | trackItem,
81 | activeToolboxItem
82 | })}
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | const ActiveControlItem = ({
90 | trackItem,
91 | activeToolboxItem
92 | }: {
93 | trackItem?: ITrackItemAndDetails;
94 | activeToolboxItem?: string;
95 | }) => {
96 | if (!trackItem || !activeToolboxItem) {
97 | return null;
98 | }
99 | return (
100 | <>
101 | {
102 | {
103 | "basic-text": (
104 |
105 | ),
106 | "basic-image": (
107 |
108 | ),
109 | "basic-video": (
110 |
111 | ),
112 | "basic-audio": (
113 |
114 | ),
115 | "preset-text": ,
116 | animation: ,
117 | smart:
118 | }[activeToolboxItem]
119 | }
120 | >
121 | );
122 | };
123 |
124 | export const ControlItem = () => {
125 | return (
126 |
127 |
128 |
129 | );
130 | };
131 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/index.tsx:
--------------------------------------------------------------------------------
1 | export { ControlItem } from './control-item';
2 |
--------------------------------------------------------------------------------
/src/pages/editor/control-item/presets.tsx:
--------------------------------------------------------------------------------
1 | const Presets = () => {
2 | return (
3 |
8 | );
9 | };
10 |
11 | export default Presets;
12 |
--------------------------------------------------------------------------------
/src/pages/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/pages/editor/editor.tsx:
--------------------------------------------------------------------------------
1 | import Timeline from "./timeline";
2 | import useStore from "../../store/store";
3 | import Navbar from "./navbar";
4 | import MenuList from "./menu-list";
5 | import { MenuItem } from "./menu-item";
6 | import useTimelineEvents from "@/hooks/use-timeline-events";
7 | import Scene from "./scene";
8 | import StateManager from "@designcombo/state";
9 | import { ControlItem } from "./control-item";
10 | import ControlList from "./control-list";
11 |
12 | const stateManager = new StateManager();
13 |
14 | function App() {
15 | const { playerRef } = useStore();
16 |
17 | useTimelineEvents();
18 |
19 | return (
20 |
21 |
22 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {playerRef && }
39 |
40 |
41 | );
42 | }
43 |
44 | export default App;
45 |
--------------------------------------------------------------------------------
/src/pages/editor/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./editor";
2 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/audios.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollArea } from "@/components/ui/scroll-area";
2 | import { AUDIOS } from "@/data/audio";
3 | import { ADD_AUDIO, dispatch } from "@designcombo/events";
4 | import { generateId } from "@designcombo/timeline";
5 | import { Music } from "lucide-react";
6 |
7 | export const Audios = () => {
8 | const handleAddAudio = () => {
9 | dispatch(ADD_AUDIO, {
10 | payload: {
11 | id: generateId(),
12 | details: {
13 | src: "https://ik.imagekit.io/snapmotion/timer-voice.mp3"
14 | }
15 | },
16 | options: {}
17 | });
18 | };
19 |
20 | return (
21 |
22 |
23 | Audios
24 |
25 |
26 |
27 | {AUDIOS.map((audio, index) => {
28 | return (
29 |
34 | );
35 | })}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const AudioItem = ({
43 | audio,
44 | handleAddAudio
45 | }: {
46 | audio: any;
47 | handleAddAudio: (src: string) => void;
48 | }) => {
49 | return (
50 | handleAddAudio(audio.src)}
52 | style={{
53 | display: "grid",
54 | gridTemplateColumns: "48px 1fr"
55 | }}
56 | className="flex px-2 py-1 gap-4 text-sm hover:bg-zinc-800/70 cursor-pointer"
57 | >
58 |
59 |
60 |
61 |
62 |
{audio.name}
63 |
{audio.author}
64 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/captions.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import captionsData from "./combo.json";
3 | import { getCaptionLines, getCaptions } from "@/utils/captions";
4 | import { ADD_CAPTION, dispatch } from "@designcombo/events";
5 | import { loadFonts } from "../utils/fonts";
6 | // interface Job {
7 | // id: string;
8 | // projectId: string;
9 | // fileName: string;
10 | // url?: string;
11 | // output?: string;
12 | // }
13 | export const Captions = () => {
14 | // const { activeIds, trackItemDetailsMap } = useStore();
15 |
16 | const generateCaptions = async () => {
17 | // https://cdn.designcombo.dev/fonts/theboldfont.ttf
18 | await loadFonts([
19 | {
20 | name: "theboldfont",
21 | url: "https://cdn.designcombo.dev/fonts/theboldfont.ttf"
22 | }
23 | ]);
24 | const captionLines = getCaptionLines(captionsData, 64, "theboldfont", 800);
25 | // console.log({ captions: captionLines });
26 | const captions = getCaptions(captionLines);
27 | // console.log({ captions: captions.slice(8, 9) });
28 | dispatch(ADD_CAPTION, {
29 | payload: captions
30 | });
31 | // console.log({ data });
32 | // const [id] = activeIds;
33 | // const trackItem = trackItemDetailsMap[id];
34 | // const src = trackItem.details.src;
35 | // console.log(trackItem);
36 | // POST https://transcribe.designcombo.dev
37 | // {
38 | // "url": "https://dev-drawify-v3.s3.eu-west-3.amazonaws.com/images/video.mp4",
39 | // "projectId": "asdasdasfdsf"
40 | // }
41 | // const reso = await axios.post(
42 | // "https://transcribe.designcombo.dev",
43 | // {
44 | // url: "https://cdn.designcombo.dev/videos/a-real-code-red-at-becca-s-school.mp4",
45 | // projectId: "asdasdasfdsf"
46 | // // mode: "test"
47 | // },
48 | // {
49 | // headers: {
50 | // Authorization: `Bearer RMU2FsdGVkX1/lPghp6YisxRFm+W2KcVcwrx1SYVD5N3O/g5NkxD3eq2TidsPih5do2epq3yyZfdVxPT0z8LWN3J/W/xtSEze/6snUgLhq5ccevl6pCNuvCcOn62pNsuXJ`
51 | // }
52 | // }
53 | // );
54 | // console.log({ reso });
55 | // const res = await axios.get(
56 | // "https://transcribe.designcombo.dev/status/86",
57 | // {
58 | // headers: {
59 | // Authorization: `Bearer RMU2FsdGVkX1/lPghp6YisxRFm+W2KcVcwrx1SYVD5N3O/g5NkxD3eq2TidsPih5do2epq3yyZfdVxPT0z8LWN3J/W/xtSEze/6snUgLhq5ccevl6pCNuvCcOn62pNsuXJ`
60 | // }
61 | // }
62 | // );
63 | // console.log({ res });
64 | // RMU2FsdGVkX1/lPghp6YisxRFm+W2KcVcwrx1SYVD5N3O/g5NkxD3eq2TidsPih5do2epq3yyZfdVxPT0z8LWN3J/W/xtSEze/6snUgLhq5ccevl6pCNuvCcOn62pNsuXJ
65 | // response object
66 | // {
67 | // "projectId": "asdasdasfdsf",
68 | // "fileName": "15a72516-45a9-476c-b07b-5edd85b994ac/asdasdasfdsf/6E3kbpMCd91H.json",
69 | // "key": " ",
70 | // "url": "https://transcribe-snapmotion.s3.us-east-1.amazonaws.com/15a72516-45a9-476c-b07b-5edd85b994ac/asdasdasfdsf/6E3kbpMCd91H.json"
71 | // }
72 | // const transcribe = data.transcribe;
73 | // console.log(transcribe);
74 | // console.log({ job });
75 | };
76 |
77 | return (
78 |
79 |
80 | Captions
81 |
82 |
83 |
84 | Recognize speech in the selected video/audio and generate captions
85 | automatically.
86 |
87 |
88 | Generate
89 |
90 |
91 |
92 | );
93 | };
94 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/elements.tsx:
--------------------------------------------------------------------------------
1 | export const Elements = () => {
2 | return (
3 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/images.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollArea } from "@/components/ui/scroll-area";
2 | import { IMAGES } from "@/data/images";
3 | import { ADD_IMAGE, dispatch } from "@designcombo/events";
4 | import { generateId } from "@designcombo/timeline";
5 |
6 | export const Images = () => {
7 | const handleAddImage = (src: string) => {
8 | dispatch(ADD_IMAGE, {
9 | payload: {
10 | id: generateId(),
11 | details: {
12 | src: src
13 | }
14 | },
15 | options: {
16 | trackId: "main"
17 | }
18 | });
19 | };
20 |
21 | return (
22 |
23 |
24 | Photos
25 |
26 |
27 |
28 | {IMAGES.map((image, index) => {
29 | return (
30 |
handleAddImage(image.src)}
32 | key={index}
33 | className="flex items-center justify-center w-full bg-background pb-2 overflow-hidden cursor-pointer"
34 | >
35 |
40 |
41 | );
42 | })}
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/index.tsx:
--------------------------------------------------------------------------------
1 | export { MenuItem } from './menu-item';
2 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/menu-item.tsx:
--------------------------------------------------------------------------------
1 | import useLayoutStore from "@/store/use-layout-store";
2 | import { Transitions } from "./transitions";
3 | import { Texts } from "./texts";
4 | import { Uploads } from "./uploads";
5 | import { Audios } from "./audios";
6 | import { Elements } from "./elements";
7 | import { Images } from "./images";
8 | import { Videos } from "./videos";
9 | import { X } from "lucide-react";
10 | import { Button } from "@/components/ui/button";
11 | import { Captions } from "./captions";
12 |
13 | const Container = ({ children }: { children: React.ReactNode }) => {
14 | const { showMenuItem, setShowMenuItem } = useLayoutStore();
15 | return (
16 |
24 |
25 |
26 |
31 | setShowMenuItem(false)} />
32 |
33 | {children}
34 |
35 |
36 | );
37 | };
38 |
39 | const ActiveMenuItem = () => {
40 | const { activeMenuItem } = useLayoutStore();
41 | if (activeMenuItem === "transitions") {
42 | return ;
43 | }
44 | if (activeMenuItem === "texts") {
45 | return ;
46 | }
47 | if (activeMenuItem === "shapes") {
48 | return ;
49 | }
50 | if (activeMenuItem === "videos") {
51 | return ;
52 | }
53 | if (activeMenuItem === "captions") {
54 | return ;
55 | }
56 |
57 | if (activeMenuItem === "audios") {
58 | return ;
59 | }
60 |
61 | if (activeMenuItem === "images") {
62 | return ;
63 | }
64 | if (activeMenuItem === "uploads") {
65 | return ;
66 | }
67 | return null;
68 | };
69 |
70 | export const MenuItem = () => {
71 | return (
72 |
73 |
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/texts.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { DEFAULT_FONT } from "@/data/fonts";
3 | import { ADD_TEXT, dispatch } from "@designcombo/events";
4 | import { generateId } from "@designcombo/timeline";
5 |
6 | export const Texts = () => {
7 | const handleAddText = () => {
8 | dispatch(ADD_TEXT, {
9 | payload: {
10 | id: generateId(),
11 | display: {
12 | from: 0,
13 | to: 1000
14 | },
15 | details: {
16 | text: "Heading and some body",
17 | fontSize: 120,
18 | width: 600,
19 | fontUrl: DEFAULT_FONT.url,
20 | fontFamily: DEFAULT_FONT.postScriptName,
21 | color: "#ffffff",
22 | wordWrap: "break-word",
23 | textAlign: "center",
24 | borderWidth: 0,
25 | borderColor: "#000000",
26 | boxShadow: {
27 | color: "#ffffff",
28 | x: 0,
29 | y: 0,
30 | blur: 0
31 | }
32 | }
33 | },
34 | options: {}
35 | });
36 | };
37 |
38 | return (
39 |
40 |
41 | Text
42 |
43 |
44 |
45 | Add text
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/transitions.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DRAG_END,
3 | DRAG_PREFIX,
4 | DRAG_START,
5 | filter,
6 | subject
7 | } from "@designcombo/events";
8 | import React, { useEffect, useState } from "react";
9 | import Draggable from "@/components/shared/draggable";
10 | import { ScrollArea } from "@/components/ui/scroll-area";
11 | import { TRANSITIONS } from "@/data/transitions";
12 |
13 | export const Transitions = () => {
14 | const [shouldDisplayPreview, setShouldDisplayPreview] = useState(true);
15 | // handle track and track item events - updates
16 | useEffect(() => {
17 | const dragEvents = subject.pipe(
18 | filter(({ key }) => key.startsWith(DRAG_PREFIX))
19 | );
20 |
21 | const dragEventsSubscription = dragEvents.subscribe((obj) => {
22 | if (obj.key === DRAG_START) {
23 | setShouldDisplayPreview(false);
24 | } else if (obj.key === DRAG_END) {
25 | setShouldDisplayPreview(true);
26 | }
27 | });
28 | return () => dragEventsSubscription.unsubscribe();
29 | }, []);
30 | return (
31 |
32 |
33 | Transitions
34 |
35 |
36 |
37 | {TRANSITIONS.map((transition, index) => (
38 |
43 | ))}
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | const TransitionsMenuItem = ({
51 | transition,
52 | shouldDisplayPreview
53 | }: {
54 | transition: Partial;
55 | shouldDisplayPreview: boolean;
56 | }) => {
57 | const style = React.useMemo(
58 | () => ({
59 | backgroundImage: `url(${transition.preview})`,
60 | backgroundSize: "cover",
61 | width: "70px",
62 | height: "70px"
63 | }),
64 | [transition.preview]
65 | );
66 |
67 | return (
68 | }
71 | shouldDisplayPreview={shouldDisplayPreview}
72 | >
73 |
74 |
81 |
82 | {transition.name || transition.type}
83 |
84 |
85 |
86 | );
87 | };
88 |
89 | export default TransitionsMenuItem;
90 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/uploads.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
3 | import { UploadIcon } from "lucide-react";
4 | import { useRef } from "react";
5 | import { ScrollArea } from "@/components/ui/scroll-area";
6 |
7 | export const Uploads = () => {
8 | const inputFileRef = useRef(null);
9 |
10 | const onInputFileChange = () => {};
11 | return (
12 |
13 |
14 | Your media
15 |
16 |
23 |
24 |
25 |
26 |
27 | Project
28 | Workspace
29 |
30 |
31 | {
33 | inputFileRef.current?.click();
34 | }}
35 | className="flex gap-2 w-full"
36 | variant="secondary"
37 | >
38 | Upload
39 |
40 |
41 |
42 |
43 | {
45 | inputFileRef.current?.click();
46 | }}
47 | className="flex gap-2 w-full"
48 | variant="secondary"
49 | >
50 | Upload
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/src/pages/editor/menu-item/videos.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollArea } from "@/components/ui/scroll-area";
2 | import { VIDEOS } from "@/data/video";
3 | import { ADD_VIDEO, dispatch } from "@designcombo/events";
4 | import { generateId } from "@designcombo/timeline";
5 |
6 | export const Videos = () => {
7 | const handleAddVideo = (src: string) => {
8 | dispatch(ADD_VIDEO, {
9 | payload: {
10 | id: generateId(),
11 | details: {
12 | src: src
13 | },
14 | metadata: {
15 | resourceId: src
16 | }
17 | },
18 | options: {
19 | resourceId: "main"
20 | }
21 | });
22 | };
23 |
24 | return (
25 |
26 |
27 | Videos
28 |
29 |
30 |
31 | {VIDEOS.map((image, index) => {
32 | return (
33 |
handleAddVideo(image.src)}
35 | key={index}
36 | className="flex items-center justify-center w-full bg-background pb-2 overflow-hidden cursor-pointer"
37 | >
38 |
43 |
44 | );
45 | })}
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/pages/editor/player/composition.tsx:
--------------------------------------------------------------------------------
1 | import useStore from "@/store/store";
2 | import { SequenceItem } from "./sequence-item";
3 | import { IItem, ITrackItem } from "@designcombo/types";
4 |
5 | const Composition = () => {
6 | const { trackItemIds, trackItemsMap, fps, trackItemDetailsMap } = useStore();
7 | return (
8 | <>
9 | {trackItemIds.map((id) => {
10 | const item = trackItemsMap[id];
11 | const itemDetails = trackItemDetailsMap[id];
12 | if (!item || !itemDetails) return;
13 | const trackItem = {
14 | ...item,
15 | details: itemDetails.details
16 | } as ITrackItem & IItem;
17 | return SequenceItem[trackItem.type](trackItem, {
18 | fps
19 | });
20 | })}
21 | >
22 | );
23 | };
24 |
25 | export default Composition;
26 |
--------------------------------------------------------------------------------
/src/pages/editor/player/editable-text.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react";
2 |
3 | const TextLayer: React.FC<{
4 | id: string;
5 | content: string;
6 | onChange?: (id: string, content: string) => void;
7 | style?: React.CSSProperties;
8 | editable?: boolean;
9 | }> = ({ id, content, editable, style = {} }) => {
10 | const [data, setData] = useState(content);
11 | const divRef = useRef(null);
12 |
13 | useEffect(() => {
14 | if (editable && divRef.current) {
15 | const element = divRef.current;
16 | element.focus();
17 | const selection = window.getSelection();
18 | const range = document.createRange();
19 | range.selectNodeContents(element);
20 | selection?.removeAllRanges();
21 | selection?.addRange(range);
22 | } else {
23 | const selection = window.getSelection();
24 | selection?.removeAllRanges();
25 | }
26 | }, [editable]);
27 |
28 | useEffect(() => {
29 | if (data !== content) {
30 | setData(content);
31 | }
32 | }, [content]);
33 | // Function to move caret to the end
34 | const moveCaretToEnd = () => {
35 | if (divRef.current) {
36 | const selection = window.getSelection();
37 | const range = document.createRange();
38 | range.selectNodeContents(divRef.current);
39 | range.collapse(false); // Collapse the range to the end of the content
40 | selection?.removeAllRanges();
41 | selection?.addRange(range);
42 | }
43 | };
44 |
45 | // OnClick handler to move caret if all text is selected
46 | const handleClick = (e: React.MouseEvent) => {
47 | e.stopPropagation();
48 | const selection = window.getSelection();
49 | const element = divRef.current;
50 |
51 | if (selection?.rangeCount && element) {
52 | const range = selection.getRangeAt(0);
53 | if (range.endOffset - range.startOffset === element.textContent?.length) {
54 | // All text is selected, move caret to the end
55 | moveCaretToEnd();
56 | }
57 | }
58 | };
59 | return (
60 |
77 | );
78 | };
79 |
80 | export default TextLayer;
81 |
--------------------------------------------------------------------------------
/src/pages/editor/player/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Player } from "./player";
2 |
--------------------------------------------------------------------------------
/src/pages/editor/player/main-layer-background.tsx:
--------------------------------------------------------------------------------
1 | const MainLayerBackground = ({ background }: { background: string }) => {
2 | return (
3 |
14 | );
15 | };
16 |
17 | export default MainLayerBackground;
18 |
--------------------------------------------------------------------------------
/src/pages/editor/player/player.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import Composition from "./composition";
3 | import { Player as RemotionPlayer, PlayerRef } from "@remotion/player";
4 | import useStore from "@/store/store";
5 |
6 | const Player = () => {
7 | const playerRef = useRef(null);
8 | const { setPlayerRef, duration, fps } = useStore();
9 |
10 | useEffect(() => {
11 | setPlayerRef(playerRef);
12 | }, []);
13 |
14 | return (
15 |
25 | );
26 | };
27 | export default Player;
28 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/index.ts:
--------------------------------------------------------------------------------
1 | export { default } from "./timeline";
2 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/items/audio.ts:
--------------------------------------------------------------------------------
1 | import { Audio as AudioBase, AudioProps } from "@designcombo/timeline";
2 |
3 | class Audio extends AudioBase {
4 | static type = "Audio";
5 | constructor(props: AudioProps) {
6 | super(props);
7 | // this.fill = "#2563eb";
8 | }
9 |
10 | public _render(ctx: CanvasRenderingContext2D) {
11 | super._render(ctx);
12 | this.drawTextIdentity(ctx);
13 | this.updateSelected(ctx);
14 | }
15 |
16 | public drawTextIdentity(ctx: CanvasRenderingContext2D) {
17 | const iconPath = new Path2D(
18 | "M8.24092 0C8.24092 2.51565 10.2795 4.55419 12.7951 4.55419C12.9677 4.55419 13.1331 4.62274 13.2552 4.74475C13.3772 4.86676 13.4457 5.03224 13.4457 5.20479C13.4457 5.37734 13.3772 5.54282 13.2552 5.66483C13.1331 5.78685 12.9677 5.85539 12.7951 5.85539C11.9218 5.85605 11.0594 5.66105 10.2713 5.28471C9.48319 4.90838 8.78942 4.36027 8.24092 3.68066V13.8794C8.24094 14.8271 7.91431 15.7458 7.31606 16.4808C6.71781 17.2157 5.88451 17.722 4.95657 17.9143C4.02863 18.1066 3.06276 17.9731 2.22172 17.5364C1.38067 17.0997 0.715856 16.3865 0.339286 15.5169C-0.0372842 14.6473 -0.10259 13.6744 0.154372 12.7622C0.411334 11.8501 0.974857 11.0544 1.74999 10.5092C2.52512 9.96403 3.46449 9.7027 4.40981 9.76924C5.35512 9.83579 6.24861 10.2261 6.93972 10.8745V0H8.24092ZM6.93972 13.8794C6.93972 13.1317 6.6427 12.4146 6.11398 11.8859C5.58527 11.3572 4.86818 11.0602 4.12046 11.0602C3.37275 11.0602 2.65566 11.3572 2.12694 11.8859C1.59823 12.4146 1.3012 13.1317 1.3012 13.8794C1.3012 14.6272 1.59823 15.3443 2.12694 15.873C2.65566 16.4017 3.37275 16.6987 4.12046 16.6987C4.86818 16.6987 5.58527 16.4017 6.11398 15.873C6.6427 15.3443 6.93972 14.6272 6.93972 13.8794Z"
19 | );
20 | ctx.save();
21 | ctx.translate(-this.width / 2, -this.height / 2);
22 | ctx.translate(0, 10);
23 | ctx.font = "600 12px 'Geist variable'";
24 | ctx.fillStyle = "#f4f4f5";
25 | ctx.textAlign = "left";
26 | ctx.clip();
27 | ctx.fillText("Audio", 36, 14);
28 |
29 | ctx.translate(10, 1);
30 |
31 | ctx.fillStyle = "#f4f4f5";
32 | ctx.fill(iconPath);
33 | ctx.restore();
34 | }
35 | }
36 |
37 | //
38 | //
39 | //
40 |
41 | export default Audio;
42 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/items/caption.ts:
--------------------------------------------------------------------------------
1 | import { Caption as CaptionBase, CaptionsProps } from "@designcombo/timeline";
2 |
3 | class Caption extends CaptionBase {
4 | static type = "Caption";
5 | constructor(props: CaptionsProps) {
6 | super(props);
7 | this.fill = "#303030";
8 | }
9 |
10 | public _render(ctx: CanvasRenderingContext2D) {
11 | super._render(ctx);
12 | this.drawTextIdentity(ctx);
13 | this.updateSelected(ctx);
14 | }
15 |
16 | public drawTextIdentity(ctx: CanvasRenderingContext2D) {
17 | const textPath = new Path2D(
18 | "M4 4.8C3.55817 4.8 3.2 5.15817 3.2 5.6C3.2 6.04183 3.55817 6.4 4 6.4H5.6C6.04183 6.4 6.4 6.04183 6.4 5.6C6.4 5.15817 6.04183 4.8 5.6 4.8H4Z M8.8 4.8C8.35817 4.8 8 5.15817 8 5.6C8 6.04183 8.35817 6.4 8.8 6.4H12C12.4418 6.4 12.8 6.04183 12.8 5.6C12.8 5.15817 12.4418 4.8 12 4.8H8.8Z M4 8C3.55817 8 3.2 8.35817 3.2 8.8C3.2 9.24183 3.55817 9.6 4 9.6H7.2C7.64183 9.6 8 9.24183 8 8.8C8 8.35817 7.64183 8 7.2 8H4Z M10.4 8C9.95817 8 9.6 8.35817 9.6 8.8C9.6 9.24183 9.95817 9.6 10.4 9.6H12C12.4418 9.6 12.8 9.24183 12.8 8.8C12.8 8.35817 12.4418 8 12 8H10.4Z M2.4 0C1.07452 0 0 1.07452 0 2.4V10.4C0 11.7255 1.07452 12.8 2.4 12.8H13.6C14.9255 12.8 16 11.7255 16 10.4V2.4C16 1.07452 14.9255 0 13.6 0H2.4ZM1.6 2.4C1.6 1.95817 1.95817 1.6 2.4 1.6H13.6C14.0418 1.6 14.4 1.95817 14.4 2.4V10.4C14.4 10.8418 14.0418 11.2 13.6 11.2H2.4C1.95817 11.2 1.6 10.8418 1.6 10.4V2.4Z"
19 | );
20 | ctx.save();
21 | ctx.translate(-this.width / 2, -this.height / 2);
22 | ctx.translate(0, 12);
23 | ctx.font = "600 12px 'Geist variable'";
24 | ctx.fillStyle = "#f4f4f5";
25 | ctx.textAlign = "left";
26 | ctx.clip();
27 | ctx.fillText(this.text, 36, 12);
28 |
29 | ctx.translate(8, 1);
30 |
31 | ctx.fillStyle = "#f4f4f5";
32 | ctx.fill(textPath);
33 | ctx.restore();
34 | }
35 | }
36 |
37 | export default Caption;
38 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/items/image.ts:
--------------------------------------------------------------------------------
1 | import { Image as ImageBase, ImageProps } from "@designcombo/timeline";
2 |
3 | class Image extends ImageBase {
4 | static type = "Image";
5 | constructor(props: ImageProps) {
6 | super(props);
7 | // this.fill = "#2563eb";
8 | }
9 |
10 | public _render(ctx: CanvasRenderingContext2D) {
11 | super._render(ctx);
12 | this.drawTextIdentity(ctx);
13 | this.updateSelected(ctx);
14 | }
15 |
16 | public drawTextIdentity(ctx: CanvasRenderingContext2D) {
17 | const iconPath = new Path2D(
18 | "M1.55556 0H14.4444C15.3031 0 16 0.696889 16 1.55556V14.4444C16 14.857 15.8361 15.2527 15.5444 15.5444C15.2527 15.8361 14.857 16 14.4444 16H1.55556C1.143 16 0.747335 15.8361 0.455612 15.5444C0.163889 15.2527 0 14.857 0 14.4444V1.55556C0 0.696889 0.696889 0 1.55556 0ZM14.4444 1.33333H1.55556C1.49662 1.33333 1.4401 1.35675 1.39842 1.39842C1.35675 1.4401 1.33333 1.49662 1.33333 1.55556V14.4444C1.33333 14.5671 1.43289 14.6667 1.55556 14.6667H1.72444L10.456 5.93511C10.6004 5.79065 10.7719 5.67605 10.9607 5.59787C11.1494 5.51968 11.3517 5.47944 11.556 5.47944C11.7603 5.47944 11.9626 5.51968 12.1513 5.59787C12.3401 5.67605 12.5116 5.79065 12.656 5.93511L14.6667 7.94578V1.55556C14.6667 1.49662 14.6433 1.4401 14.6016 1.39842C14.5599 1.35675 14.5034 1.33333 14.4444 1.33333ZM14.6667 9.83111L11.7129 6.87733C11.6922 6.85664 11.6677 6.84022 11.6407 6.82902C11.6137 6.81781 11.5848 6.81205 11.5556 6.81205C11.5263 6.81205 11.4974 6.81781 11.4704 6.82902C11.4434 6.84022 11.4189 6.85664 11.3982 6.87733L3.60978 14.6667H14.4444C14.5034 14.6667 14.5599 14.6433 14.6016 14.6016C14.6433 14.5599 14.6667 14.5034 14.6667 14.4444V9.83111ZM4.88889 7.11111C4.29952 7.11111 3.73429 6.87699 3.31754 6.46024C2.90079 6.04349 2.66667 5.47826 2.66667 4.88889C2.66667 4.29952 2.90079 3.73429 3.31754 3.31754C3.73429 2.90079 4.29952 2.66667 4.88889 2.66667C5.47826 2.66667 6.04349 2.90079 6.46024 3.31754C6.87699 3.73429 7.11111 4.29952 7.11111 4.88889C7.11111 5.47826 6.87699 6.04349 6.46024 6.46024C6.04349 6.87699 5.47826 7.11111 4.88889 7.11111ZM4.88889 5.77778C5.12464 5.77778 5.35073 5.68413 5.51743 5.51743C5.68413 5.35073 5.77778 5.12464 5.77778 4.88889C5.77778 4.65314 5.68413 4.42705 5.51743 4.26035C5.35073 4.09365 5.12464 4 4.88889 4C4.65314 4 4.42705 4.09365 4.26035 4.26035C4.09365 4.42705 4 4.65314 4 4.88889C4 5.12464 4.09365 5.35073 4.26035 5.51743C4.42705 5.68413 4.65314 5.77778 4.88889 5.77778Z"
19 | );
20 | ctx.save();
21 | ctx.translate(-this.width / 2, -this.height / 2);
22 | ctx.translate(0, 12);
23 | ctx.font = "600 12px 'Geist variable'";
24 | ctx.fillStyle = "#f4f4f5";
25 | ctx.textAlign = "left";
26 | ctx.clip();
27 | ctx.fillText("Image", 36, 12);
28 |
29 | ctx.translate(8, 1);
30 |
31 | ctx.fillStyle = "#f4f4f5";
32 | ctx.fill(iconPath);
33 | ctx.restore();
34 | }
35 | }
36 |
37 | export default Image;
38 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/items/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Text } from "./text";
2 | export { default as Image } from "./image";
3 | export { default as Audio } from "./audio";
4 | export { default as Video } from "./video";
5 | export { default as Caption } from "./caption";
6 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/items/text.ts:
--------------------------------------------------------------------------------
1 | import { Text as TextBase, TextProps } from "@designcombo/timeline";
2 |
3 | class Text extends TextBase {
4 | static type = "Text";
5 | constructor(props: TextProps) {
6 | super(props);
7 | this.fill = "#303030";
8 | }
9 |
10 | public _render(ctx: CanvasRenderingContext2D) {
11 | super._render(ctx);
12 | this.drawTextIdentity(ctx);
13 | this.updateSelected(ctx);
14 | }
15 |
16 | public drawTextIdentity(ctx: CanvasRenderingContext2D) {
17 | const textPath = new Path2D(
18 | "M6.23982 0.361968C6.18894 0.253743 6.10832 0.162234 6.00736 0.0981357C5.9064 0.034038 5.78929 0 5.6697 0C5.55012 0 5.433 0.034038 5.33204 0.0981357C5.23109 0.162234 5.15046 0.253743 5.09959 0.361968L0.0599035 11.0713C0.0246926 11.1462 0.00457285 11.2272 0.000693114 11.3099C-0.00318662 11.3925 0.00924959 11.4751 0.0372917 11.553C0.0939253 11.7102 0.210687 11.8384 0.361891 11.9095C0.513095 11.9806 0.686354 11.9888 0.843555 11.9322C1.00076 11.8755 1.12902 11.7588 1.20013 11.6075L2.51202 8.81998H8.82738L10.1393 11.6075C10.1745 11.6824 10.2241 11.7496 10.2853 11.8053C10.3465 11.861 10.418 11.9041 10.4958 11.9322C10.5737 11.9602 10.6563 11.9726 10.7389 11.9687C10.8216 11.9649 10.9026 11.9447 10.9775 11.9095C11.0524 11.8743 11.1196 11.8247 11.1753 11.7635C11.231 11.7023 11.2741 11.6308 11.3021 11.553C11.3302 11.4751 11.3426 11.3925 11.3387 11.3099C11.3348 11.2272 11.3147 11.1462 11.2795 11.0713L6.23982 0.361968ZM3.10498 7.56005L5.6697 2.11011L8.23443 7.56005H3.10498ZM15.1191 3.78029C14.1143 3.78029 13.3292 4.05354 12.7859 4.59294C12.6721 4.71153 12.6092 4.86987 12.6106 5.03419C12.6119 5.19851 12.6774 5.3558 12.7931 5.4725C12.9088 5.58921 13.0655 5.6561 13.2298 5.6589C13.3941 5.6617 13.553 5.60018 13.6726 5.48748C13.9718 5.19062 14.46 5.04021 15.1191 5.04021C16.1609 5.04021 17.009 5.74892 17.009 6.61511V6.86867C16.45 6.49465 15.7917 6.29663 15.1191 6.30013C13.382 6.30013 11.9693 7.57187 11.9693 9.13495C11.9693 10.698 13.382 11.9698 15.1191 11.9698C15.792 11.9727 16.4503 11.7739 17.009 11.3989C17.0168 11.566 17.0907 11.7231 17.2144 11.8357C17.3381 11.9483 17.5014 12.0071 17.6685 11.9993C17.8356 11.9915 17.9927 11.9176 18.1053 11.7939C18.2179 11.6702 18.2767 11.5069 18.2689 11.3398V6.61511C18.2689 5.05202 16.8562 3.78029 15.1191 3.78029ZM15.1191 10.7099C14.0773 10.7099 13.2292 10.0012 13.2292 9.13495C13.2292 8.26876 14.0773 7.56005 15.1191 7.56005C16.1609 7.56005 17.009 8.26876 17.009 9.13495C17.009 10.0012 16.1609 10.7099 15.1191 10.7099Z"
19 | );
20 | ctx.save();
21 | ctx.translate(-this.width / 2, -this.height / 2);
22 | ctx.translate(0, 12);
23 | ctx.font = "600 12px 'Geist variable'";
24 | ctx.fillStyle = "#f4f4f5";
25 | ctx.textAlign = "left";
26 | ctx.clip();
27 | ctx.fillText(this.text, 36, 12);
28 |
29 | ctx.translate(8, 1);
30 |
31 | ctx.fillStyle = "#f4f4f5";
32 | ctx.fill(textPath);
33 | ctx.restore();
34 | }
35 | }
36 |
37 | export default Text;
38 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/items/video.ts:
--------------------------------------------------------------------------------
1 | import { Video as VideoBase, VideoProps } from "@designcombo/timeline";
2 |
3 | class Video extends VideoBase {
4 | static type = "Video";
5 | constructor(props: VideoProps) {
6 | super(props);
7 | // this.fill = "#2563eb";
8 | }
9 |
10 | public _render(ctx: CanvasRenderingContext2D) {
11 | super._render(ctx);
12 | this.drawTextIdentity(ctx);
13 | this.updateSelected(ctx);
14 | }
15 |
16 | public drawTextIdentity(ctx: CanvasRenderingContext2D) {
17 | const iconPath = new Path2D(
18 | "M16.5625 0.925L12.5 3.275V0.625L11.875 0H0.625L0 0.625V9.375L0.625 10H11.875L12.5 9.375V6.875L16.5625 9.2125L17.5 8.625V1.475L16.5625 0.925ZM11.25 8.75H1.25V1.25H11.25V8.75ZM16.25 7.5L12.5 5.375V4.725L16.25 2.5V7.5Z"
19 | );
20 | ctx.save();
21 | ctx.translate(-this.width / 2, -this.height / 2);
22 | ctx.translate(0, 14);
23 | ctx.font = "600 12px 'Geist variable'";
24 | ctx.fillStyle = "#f4f4f5";
25 | ctx.textAlign = "left";
26 | ctx.clip();
27 | ctx.fillText("Video", 36, 10);
28 |
29 | ctx.translate(8, 1);
30 |
31 | ctx.fillStyle = "#f4f4f5";
32 | ctx.fill(iconPath);
33 | ctx.restore();
34 | }
35 | }
36 |
37 | export default Video;
38 |
--------------------------------------------------------------------------------
/src/pages/editor/timeline/playhead.tsx:
--------------------------------------------------------------------------------
1 | import { useCurrentPlayerFrame } from "@/hooks/use-current-frame";
2 | import useStore from "@/store/store";
3 | import { timeMsToUnits, unitsToTimeMs } from "@designcombo/timeline";
4 | import { MouseEvent, TouchEvent, useEffect, useRef, useState } from "react";
5 |
6 | const Playhead = ({ scrollLeft }: { scrollLeft: number }) => {
7 | const playheadRef = useRef(null);
8 | const { playerRef, fps, scale } = useStore();
9 | const currentFrame = useCurrentPlayerFrame(playerRef!);
10 | const position =
11 | timeMsToUnits((currentFrame / fps) * 1000, scale.zoom) - scrollLeft;
12 | const [isDragging, setIsDragging] = useState(false);
13 | const [dragStartX, setDragStartX] = useState(0);
14 | const [dragStartPosition, setDragStartPosition] = useState(position);
15 |
16 | const handleMouseUp = () => {
17 | setIsDragging(false);
18 | };
19 |
20 | const handleMouseDown = (
21 | e:
22 | | MouseEvent
23 | | TouchEvent
24 | ) => {
25 | setIsDragging(true);
26 | const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
27 | setDragStartX(clientX);
28 | setDragStartPosition(position);
29 | };
30 |
31 | const handleMouseMove = (
32 | e: globalThis.MouseEvent | globalThis.TouchEvent
33 | ) => {
34 | if (isDragging) {
35 | const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
36 | const delta = clientX - dragStartX;
37 | const newPosition = dragStartPosition + delta;
38 |
39 | const time = unitsToTimeMs(newPosition, scale.zoom);
40 | playerRef?.current?.seekTo((time * fps) / 1000);
41 | }
42 | };
43 |
44 | useEffect(() => {
45 | if (isDragging) {
46 | document.addEventListener("mousemove", handleMouseMove);
47 | document.addEventListener("mouseup", handleMouseUp);
48 | document.addEventListener("touchmove", handleMouseMove);
49 | document.addEventListener("touchend", handleMouseUp);
50 | } else {
51 | document.removeEventListener("mousemove", handleMouseMove);
52 | document.removeEventListener("mouseup", handleMouseUp);
53 | document.removeEventListener("touchmove", handleMouseMove);
54 | document.removeEventListener("touchend", handleMouseUp);
55 | }
56 |
57 | // Cleanup event listeners on component unmount
58 | return () => {
59 | document.removeEventListener("mousemove", handleMouseMove);
60 | document.removeEventListener("mouseup", handleMouseUp);
61 | document.removeEventListener("touchmove", handleMouseMove);
62 | document.removeEventListener("touchend", handleMouseUp);
63 | };
64 | }, [isDragging]);
65 |
66 | return (
67 | handleMouseDown(e)}
70 | onTouchStart={(e) => handleMouseDown(e)}
71 | style={{
72 | position: "absolute",
73 | left: 40 + position,
74 | top: 80,
75 | width: 1,
76 | height: "calc(100% - 80px)",
77 | background: "#d4d4d8",
78 | zIndex: 10,
79 | cursor: "pointer"
80 | }}
81 | >
82 |
100 |
101 | );
102 | };
103 |
104 | export default Playhead;
105 |
--------------------------------------------------------------------------------
/src/pages/editor/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/pages/editor/utils/target.ts:
--------------------------------------------------------------------------------
1 | export const getTargetControls = (targetType: string): string[] => {
2 | switch (targetType) {
3 | case "text":
4 | return ["e", "se"];
5 | case "image":
6 | return ["nw", "ne", "sw", "se"];
7 | case "svg":
8 | return ["nw", "n", "ne", "w", "e", "sw", "s", "se"];
9 | case "group":
10 | return ["nw", "ne", "sw", "se"];
11 | default:
12 | return ["nw", "ne", "sw", "se"];
13 | }
14 | };
15 |
16 | interface ITargetAbles {
17 | rotatable: boolean;
18 | resizable: boolean;
19 | scalable: boolean;
20 | keepRatio: boolean;
21 | draggable: boolean;
22 | snappable: boolean;
23 | }
24 |
25 | export const getTargetAbles = (targetType: string): ITargetAbles => {
26 | switch (targetType) {
27 | case "text":
28 | return {
29 | rotatable: true,
30 | resizable: true,
31 | scalable: false,
32 | keepRatio: false,
33 | draggable: true,
34 | snappable: true
35 | };
36 | case "image":
37 | return {
38 | rotatable: true,
39 | resizable: false,
40 | scalable: true,
41 | keepRatio: true,
42 | draggable: true,
43 | snappable: true
44 | };
45 | case "group":
46 | return {
47 | rotatable: false,
48 | resizable: false,
49 | scalable: true,
50 | keepRatio: true,
51 | draggable: true,
52 | snappable: true
53 | };
54 | case "svg":
55 | return {
56 | rotatable: true,
57 | resizable: false,
58 | scalable: true,
59 | keepRatio: true,
60 |
61 | draggable: true,
62 | snappable: true
63 | };
64 | default:
65 | return {
66 | rotatable: true,
67 | resizable: false,
68 | scalable: true,
69 | keepRatio: true,
70 | draggable: true,
71 | snappable: true
72 | };
73 | }
74 | };
75 |
76 | export const getTypeFromClassName = (input: string): string | null => {
77 | const regex = /designcombo-scene-item-type-([^ ]+)/;
78 | const match = input.match(regex);
79 | return match ? match[1] : null;
80 | };
81 |
82 | export interface SelectionInfo {
83 | targets: HTMLElement[];
84 | layerType: string | null;
85 | ables: ITargetAbles;
86 | controls: string[];
87 | }
88 |
89 | export const emptySelection: SelectionInfo = {
90 | targets: [],
91 | layerType: null,
92 | ables: {
93 | rotatable: false,
94 | resizable: false,
95 | scalable: false,
96 | keepRatio: false,
97 | draggable: true,
98 | snappable: true
99 | },
100 | controls: []
101 | };
102 |
103 | export const getSelectionByIds = (ids: string[]): SelectionInfo => {
104 | if (!ids || ids.length === 0) return emptySelection;
105 |
106 | const targets = ids
107 | .map((id) => {
108 | if (!id) return null;
109 | const element = document.querySelector(
110 | `.designcombo-scene-item.id-${id}`
111 | );
112 | return element;
113 | })
114 | .filter((target): target is HTMLElement => target !== null)
115 | .filter((target) => {
116 | const targetType = getTypeFromClassName(target.className)!;
117 | return targetType !== "audio";
118 | });
119 |
120 | if (targets.length === 0) return emptySelection;
121 | if (targets.length === 1) {
122 | const target = targets[0];
123 | const targetType = getTypeFromClassName(target.className)!;
124 | const ables = getTargetAbles(targetType);
125 | const controls = getTargetControls(targetType);
126 | return { targets: [target], layerType: targetType, ables, controls };
127 | } else {
128 | return {
129 | targets,
130 | layerType: "group",
131 | ables: getTargetAbles("group"),
132 | controls: []
133 | };
134 | }
135 | };
136 |
137 | export const getTargetById = (id: string): HTMLElement | null => {
138 | const element = document.querySelector(
139 | `.designcombo-scene-item.id-${id}`
140 | );
141 | return element;
142 | };
143 |
--------------------------------------------------------------------------------
/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import CanvasTimeline from "@designcombo/timeline";
2 | import {
3 | ITimelineScaleState,
4 | ITimelineScrollState,
5 | ITrack,
6 | ITrackItem,
7 | ITransition
8 | } from "@designcombo/types";
9 | import { PlayerRef } from "@remotion/player";
10 | import { create } from "zustand";
11 |
12 | interface ITimelineStore {
13 | duration: number;
14 | fps: number;
15 | scale: ITimelineScaleState;
16 | scroll: ITimelineScrollState;
17 |
18 | tracks: ITrack[];
19 | trackItemIds: string[];
20 | transitionIds: string[];
21 | transitionsMap: Record;
22 | trackItemsMap: Record;
23 | trackItemDetailsMap: Record;
24 | activeIds: string[];
25 | timeline: CanvasTimeline | null;
26 | setTimeline: (timeline: CanvasTimeline) => void;
27 | setScale: (scale: ITimelineScaleState) => void;
28 | setScroll: (scroll: ITimelineScrollState) => void;
29 | playerRef: React.RefObject | null;
30 | setPlayerRef: (playerRef: React.RefObject | null) => void;
31 |
32 | setState: (state: any) => Promise;
33 | }
34 |
35 | const useStore = create((set) => ({
36 | timeline: null,
37 | duration: 5000,
38 | fps: 30,
39 | scale: {
40 | // 1x distance (second 0 to second 5, 5 segments).
41 | unit: 300,
42 | zoom: 1 / 240,
43 | segments: 5
44 | },
45 | scroll: {
46 | left: 0,
47 | top: 0
48 | },
49 | playerRef: null,
50 | trackItemDetailsMap: {},
51 | activeIds: [],
52 | targetIds: [],
53 | tracks: [],
54 | trackItemIds: [],
55 | transitionIds: [],
56 | transitionsMap: {},
57 | trackItemsMap: {},
58 |
59 | setTimeline: (timeline: CanvasTimeline) =>
60 | set(() => ({
61 | timeline: timeline
62 | })),
63 | setScale: (scale: ITimelineScaleState) =>
64 | set(() => ({
65 | scale: scale
66 | })),
67 | setScroll: (scroll: ITimelineScrollState) =>
68 | set(() => ({
69 | scroll: scroll
70 | })),
71 | setState: async (state) => {
72 | return set({ ...state });
73 | },
74 | setPlayerRef: (playerRef: React.RefObject | null) =>
75 | set({ playerRef })
76 | }));
77 |
78 | export default useStore;
79 |
--------------------------------------------------------------------------------
/src/store/use-auth-store.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@/interfaces/editor";
2 | import { create } from "zustand";
3 |
4 | interface AuthStore {
5 | user: User | null;
6 | isAuthenticated: boolean;
7 | signOut: () => Promise;
8 | setUser: (user: User | null) => void;
9 | signinWithMagicLink: ({ email }: { email: string }) => Promise;
10 | signinWithGithub: () => Promise;
11 | }
12 |
13 | const useAuthStore = create((set) => ({
14 | user: null,
15 | setUser: (user) => set({ user }),
16 | isAuthenticated: false,
17 | signinWithGithub: async () => {},
18 | signinWithMagicLink: async () => {},
19 |
20 | signOut: async () => {
21 | set({ user: null, isAuthenticated: false });
22 | }
23 | }));
24 |
25 | export default useAuthStore;
26 |
--------------------------------------------------------------------------------
/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 | cropTarget: null,
8 | showControlItem: false,
9 | showToolboxItem: false,
10 | activeToolboxItem: null,
11 | setCropTarget: (cropTarget) => set({ cropTarget }),
12 | setActiveMenuItem: (showMenu) => set({ activeMenuItem: showMenu }),
13 | setShowMenuItem: (showMenuItem) => set({ showMenuItem }),
14 | setShowControlItem: (showControlItem) => set({ showControlItem }),
15 | setShowToolboxItem: (showToolboxItem) => set({ showToolboxItem }),
16 | setActiveToolboxItem: (activeToolboxItem) => set({ activeToolboxItem })
17 | }));
18 |
19 | export default useLayoutStore;
20 |
--------------------------------------------------------------------------------
/src/utils/captions.ts:
--------------------------------------------------------------------------------
1 | import { generateId } from "@designcombo/timeline";
2 | import { ICaption } from "@designcombo/types";
3 |
4 | interface Word {
5 | start: number;
6 | end: number;
7 | word: string;
8 | }
9 |
10 | interface Segment {
11 | start: number;
12 | end: number;
13 | text: string;
14 | words: Word[];
15 | }
16 |
17 | interface Input {
18 | segments: Segment[];
19 | }
20 |
21 | interface ICaptionLines {
22 | lines: Line[];
23 | }
24 |
25 | interface Line {
26 | text: string;
27 | words: Word[];
28 | width: number;
29 | start: number;
30 | end: number;
31 | }
32 |
33 | export function getCaptionLines(
34 | input: Input,
35 | fontSize: number,
36 | fontFamily: string,
37 | maxWidth: number
38 | ): ICaptionLines {
39 | const canvas = document.createElement("canvas");
40 | const context = canvas.getContext("2d")!;
41 | context.font = `${fontSize}px ${fontFamily}`;
42 |
43 | const captionLines: ICaptionLines = { lines: [] };
44 | input.segments.forEach((segment) => {
45 | let currentLine: Line = {
46 | text: "",
47 | words: [],
48 | width: 0,
49 | start: segment.start,
50 | end: 0
51 | };
52 | segment.words.forEach((wordObj, index) => {
53 | const wordWidth = context.measureText(wordObj.word).width;
54 |
55 | // Check if adding this word exceeds the max width
56 | if (currentLine.width + wordWidth > maxWidth) {
57 | // Push the current line to captionLines and start a new line
58 | console.log({ currentLine });
59 | captionLines.lines.push(currentLine);
60 | currentLine = {
61 | text: "",
62 | words: [],
63 | width: 0,
64 | start: wordObj.start,
65 | end: wordObj.end
66 | };
67 | }
68 |
69 | // Add the word to the current line
70 | currentLine.text += (currentLine.text ? " " : "") + wordObj.word;
71 | currentLine.words.push(wordObj);
72 | currentLine.width += wordWidth;
73 |
74 | // Update line end time
75 | currentLine.end = wordObj.end;
76 |
77 | // Push the last line when the iteration ends
78 | if (index === segment.words.length - 1) {
79 | captionLines.lines.push(currentLine);
80 | }
81 | });
82 | });
83 |
84 | return captionLines;
85 | }
86 |
87 | export const getCaptions = (
88 | captionLines: ICaptionLines
89 | ): Partial[] => {
90 | const captions = captionLines.lines.map((line) => {
91 | return {
92 | id: generateId(),
93 | type: "caption",
94 | name: "Caption",
95 | display: {
96 | from: line.start,
97 | to: line.end
98 | },
99 | metadata: {},
100 | details: {
101 | top: 400,
102 | text: line.text,
103 | fontSize: 64,
104 | width: 800,
105 | fontFamily: "theboldfont",
106 | fontUrl: "https://cdn.designcombo.dev/fonts/theboldfont.ttf",
107 | color: "#fff",
108 | textAlign: "center",
109 | words: line.words
110 | }
111 | };
112 | });
113 | return captions as unknown as Partial[];
114 | };
115 |
--------------------------------------------------------------------------------
/src/utils/download.ts:
--------------------------------------------------------------------------------
1 | export const download = (url: string, filename: string) => {
2 | fetch(url)
3 | .then((response) => response.blob())
4 | .then((blob) => {
5 | const url = window.URL.createObjectURL(blob);
6 | const link = document.createElement("a");
7 | link.href = url;
8 | link.setAttribute("download", `${filename}.mp4`); // Specify the filename for the downloaded video
9 | document.body.appendChild(link);
10 | link.click();
11 | link.parentNode?.removeChild(link);
12 | window.URL.revokeObjectURL(url);
13 | })
14 | .catch((error) => console.error("Download error:", error));
15 | };
16 |
--------------------------------------------------------------------------------
/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | import { PREVIEW_FRAME_WIDTH } from "@/constants";
2 |
3 | /**
4 | * Converts raw timeline units to the readable format.
5 | * @param units Target unit value.
6 | * @returns Time in format HH:MM:SS.FPS
7 | */
8 | export function formatTimelineUnit(units?: number): string {
9 | if (!units) return "0";
10 | const time = units / PREVIEW_FRAME_WIDTH;
11 |
12 | const frames = Math.trunc(time) % 60;
13 | const seconds = Math.trunc(time / 60) % 60;
14 | const minutes = Math.trunc(time / 3600) % 60;
15 | const hours = Math.trunc(time / 216000);
16 | const formattedTime = [
17 | hours.toString(),
18 | minutes.toString(),
19 | seconds.toString(),
20 | frames.toString()
21 | ];
22 |
23 | if (time < 60) {
24 | return `${formattedTime[3].padStart(2, "0")}f`;
25 | }
26 | if (time < 3600) {
27 | return `${formattedTime[2].padStart(1, "0")}s`;
28 | }
29 | if (time < 216000) {
30 | return `${formattedTime[1].padStart(2, "0")}:${formattedTime[2].padStart(2, "0")}`;
31 | }
32 | return `${formattedTime[0].padStart(2, "0")}:${formattedTime[1].padStart(2, "0")}:${formattedTime[2].padStart(2, "0")}`;
33 | }
34 |
35 | export function formatTimeToHumanReadable(
36 | ms: number,
37 | includeFrames = false
38 | ): string {
39 | if (!ms) return "00:00";
40 |
41 | const fps = 60;
42 | const msPerFrame = 1000 / fps;
43 |
44 | if (ms < 1000) {
45 | if (includeFrames) {
46 | const frames = Math.floor(ms / msPerFrame);
47 | return `${frames}f`;
48 | } else {
49 | // Convert milliseconds to seconds (with one decimal place)
50 | const seconds = (ms / 1000).toFixed(1);
51 | return `${seconds}s`;
52 | }
53 | }
54 |
55 | const seconds = Math.floor(ms / 1000);
56 | if (seconds < 60) {
57 | return `${seconds}s`;
58 | }
59 |
60 | const minutes = Math.floor(seconds / 60);
61 | if (minutes < 60) {
62 | const remainingSeconds = seconds % 60;
63 | return `${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
64 | }
65 |
66 | const hours = Math.floor(minutes / 60);
67 | const remainingMinutes = minutes % 60;
68 | const remainingSeconds = seconds % 60;
69 |
70 | return `${hours.toString().padStart(2, "0")}:${remainingMinutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
71 | }
72 |
--------------------------------------------------------------------------------
/src/utils/scene.ts:
--------------------------------------------------------------------------------
1 | export const getIdFromClassName = (input: string): string => {
2 | const regex = /designcombo-scene-item id-([^ ]+)/;
3 | const match = input.match(regex);
4 | return match ? match[1] : (null as unknown as string);
5 | };
6 |
7 | export const populateTransitionIds = (inputArray: string[]): string[] => {
8 | let newArray: string[] = [];
9 |
10 | for (let i = 0; i < inputArray.length; i++) {
11 | newArray.push(inputArray[i]);
12 | if (i < inputArray.length - 1) {
13 | newArray.push(`${inputArray[i]}-${inputArray[i + 1]}`);
14 | }
15 | }
16 |
17 | return newArray;
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/search.ts:
--------------------------------------------------------------------------------
1 | export type BinarySearchPredicate = (
2 | value: T,
3 | index: number,
4 | arr: T[]
5 | ) => boolean;
6 |
7 | /**
8 | * Searches for a value by predicate function.
9 | * @param arr The list of any values.
10 | * @param predicate Predicate function.
11 | * @returns Found index or -1.
12 | */
13 | export function findIndex(
14 | arr: T[],
15 | predicate: BinarySearchPredicate
16 | ): number {
17 | let l = -1;
18 | let r = arr.length - 1;
19 |
20 | while (1 + l < r) {
21 | const mid = l + ((r - l) >> 1);
22 | const cmp = predicate(arr[mid], mid, arr);
23 |
24 | cmp ? (r = mid) : (l = mid);
25 | }
26 |
27 | return r;
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/time.ts:
--------------------------------------------------------------------------------
1 | export const frameToTimeString = (
2 | { frame }: { frame: number },
3 | { fps }: { fps: number }
4 | ): string => {
5 | // Calculate the total time in seconds
6 | const totalSeconds = frame / fps;
7 |
8 | // Calculate hours, minutes, seconds, and milliseconds
9 | const hours = Math.floor(totalSeconds / 3600);
10 | const remainingSeconds = totalSeconds % 3600;
11 | const minutes = Math.floor(remainingSeconds / 60);
12 | const seconds = Math.floor(remainingSeconds % 60);
13 |
14 | // Format the time string based on whether hours are zero or not
15 | if (hours > 0) {
16 | return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds
17 | .toString()
18 | .padStart(2, "0")}`;
19 | } else {
20 | return `${minutes.toString().padStart(2, "0")}:${seconds
21 | .toString()
22 | .padStart(2, "0")}`;
23 | }
24 | };
25 |
26 | export const timeToString = ({ time }: { time: number }): string => {
27 | // Calculate the total time in seconds
28 | const totalSeconds = time / 1000;
29 |
30 | // Calculate hours, minutes, seconds, and milliseconds
31 | const hours = Math.floor(totalSeconds / 3600);
32 | const remainingSeconds = totalSeconds % 3600;
33 | const minutes = Math.floor(remainingSeconds / 60);
34 | const seconds = Math.floor(remainingSeconds % 60);
35 |
36 | // Format the time string based on whether hours are zero or not
37 | if (hours > 0) {
38 | return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds
39 | .toString()
40 | .padStart(2, "0")}`;
41 | } else {
42 | return `${minutes.toString().padStart(2, "0")}:${seconds
43 | .toString()
44 | .padStart(2, "0")}`;
45 | }
46 | };
47 |
48 | export const getCurrentTime = () => {
49 | const currentTimeElement = document.getElementById("video-current-time");
50 | let currentTimeSeconds = currentTimeElement
51 | ? parseFloat(currentTimeElement.getAttribute("data-current-time")!)
52 | : 0;
53 | const currentTimeMiliseconds = currentTimeSeconds * 1000;
54 | return currentTimeMiliseconds;
55 | };
56 |
--------------------------------------------------------------------------------
/src/utils/timeline.ts:
--------------------------------------------------------------------------------
1 | import { TIMELINE_ZOOM_LEVELS } from "@/constants/scale";
2 | import { findIndex } from "./search";
3 | import { FRAME_INTERVAL, PREVIEW_FRAME_WIDTH } from "@/constants";
4 | import { ITimelineScaleState } from "@designcombo/types";
5 |
6 | export function getPreviousZoomLevel(
7 | currentZoom: ITimelineScaleState
8 | ): ITimelineScaleState {
9 | return TIMELINE_ZOOM_LEVELS[getPreviousZoomIndex(currentZoom)];
10 | }
11 |
12 | export function getNextZoomLevel(
13 | currentZoom: ITimelineScaleState
14 | ): ITimelineScaleState {
15 | return TIMELINE_ZOOM_LEVELS[getNextZoomIndex(currentZoom)];
16 | }
17 |
18 | export function getPreviousZoomIndex(currentZoom: ITimelineScaleState): number {
19 | const lastLevel = TIMELINE_ZOOM_LEVELS.at(-1);
20 | const isLastIndex = currentZoom === lastLevel;
21 | const nextZoomIndex = getNextZoomIndex(currentZoom);
22 | const previousZoomIndex = nextZoomIndex - (isLastIndex ? 1 : 2);
23 |
24 | // Limit zoom to the first default level.
25 | return Math.max(0, previousZoomIndex);
26 | }
27 |
28 | export function getNextZoomIndex(currentZoom: ITimelineScaleState): number {
29 | const nextZoomIndex = findIndex(TIMELINE_ZOOM_LEVELS, (level) => {
30 | return level.zoom > currentZoom.zoom;
31 | });
32 |
33 | // Limit zoom to the last default level.
34 | return Math.min(TIMELINE_ZOOM_LEVELS.length - 1, nextZoomIndex);
35 | }
36 |
37 | export function timeMsToUnits(timeMs: number, zoom = 1): number {
38 | const zoomedFrameWidth = PREVIEW_FRAME_WIDTH * zoom;
39 | const frames = timeMs * (60 / 1000);
40 |
41 | return frames * zoomedFrameWidth;
42 | }
43 |
44 | export function unitsToTimeMs(units: number, zoom = 1): number {
45 | const zoomedFrameWidth = PREVIEW_FRAME_WIDTH * zoom;
46 |
47 | const frames = units / zoomedFrameWidth;
48 |
49 | return frames * FRAME_INTERVAL;
50 | }
51 |
52 | export function calculateTimelineWidth(
53 | totalLengthMs: number,
54 | zoom = 1
55 | ): number {
56 | return timeMsToUnits(totalLengthMs, zoom);
57 | }
58 |
--------------------------------------------------------------------------------
/src/utils/upload.ts:
--------------------------------------------------------------------------------
1 | import { generateId } from "@designcombo/timeline";
2 |
3 | const BASE_URL = "https://transcribe.designcombo.dev/presigned-url";
4 |
5 | interface IUploadDetails {
6 | uploadUrl: string;
7 | url: string;
8 | name: string;
9 | id: string;
10 | }
11 | export const createUploadsDetails = async (
12 | fileName: string
13 | ): Promise => {
14 | const currentFormat = fileName.split(".").pop();
15 | const uniqueFileName = `${generateId()}`;
16 | const updatedFileName = `${uniqueFileName}.${currentFormat}`;
17 | const response = await fetch(BASE_URL, {
18 | method: "POST",
19 | body: JSON.stringify({ fileName: updatedFileName })
20 | });
21 |
22 | const data = await response.json();
23 | return {
24 | uploadUrl: data.presigned_url as string,
25 | url: data.url as string,
26 | name: updatedFileName,
27 | id: uniqueFileName
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/src/utils/user.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@/interfaces/editor";
2 |
3 | export const getUserFromSession = (session: any): User => {
4 | return {
5 | id: session.user.id,
6 | email: session.user.email,
7 | avatar: session.user.user_metadata.avatar_url,
8 | username: session.user.user_metadata.user_name,
9 | provider: "github"
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require("tailwindcss/defaultTheme");
2 | const {
3 | default: flattenColorPalette
4 | } = require("tailwindcss/lib/util/flattenColorPalette");
5 |
6 | /** @type {import('tailwindcss').Config} */
7 | export default {
8 | darkMode: ["class"],
9 | content: ["./src/**/*.{ts,tsx}"],
10 | fontFamily: {
11 | sans: ['"Geist Variable"', ...defaultTheme.fontFamily.sans]
12 | },
13 | theme: {
14 | extend: {
15 | borderRadius: {
16 | lg: "var(--radius)",
17 | md: "calc(var(--radius) - 2px)",
18 | sm: "calc(var(--radius) - 4px)"
19 | },
20 |
21 | colors: {
22 | background: "hsl(var(--background))",
23 | foreground: "hsl(var(--foreground))",
24 | card: {
25 | DEFAULT: "hsl(var(--card))",
26 | foreground: "hsl(var(--card-foreground))"
27 | },
28 | popover: {
29 | DEFAULT: "hsl(var(--popover))",
30 | foreground: "hsl(var(--popover-foreground))"
31 | },
32 | primary: {
33 | DEFAULT: "hsl(var(--primary))",
34 | foreground: "hsl(var(--primary-foreground))"
35 | },
36 | secondary: {
37 | DEFAULT: "hsl(var(--secondary))",
38 | foreground: "hsl(var(--secondary-foreground))"
39 | },
40 | muted: {
41 | DEFAULT: "hsl(var(--muted))",
42 | foreground: "hsl(var(--muted-foreground))"
43 | },
44 | accent: {
45 | DEFAULT: "hsl(var(--accent))",
46 | foreground: "hsl(var(--accent-foreground))"
47 | },
48 | destructive: {
49 | DEFAULT: "hsl(var(--destructive))",
50 | foreground: "hsl(var(--destructive-foreground))"
51 | },
52 | scene: {
53 | DEFAULT: "hsl(var(--scene))",
54 | foreground: "hsl(var(--scene-foreground))"
55 | },
56 | border: "hsl(var(--border))",
57 | input: "hsl(var(--input))",
58 | ring: "hsl(var(--ring))"
59 | }
60 | }
61 | },
62 | plugins: [addVariablesForColors, require("tailwindcss-animate")]
63 | };
64 |
65 | function addVariablesForColors({ addBase, theme }) {
66 | let allColors = flattenColorPalette(theme("colors"));
67 | let newVars = Object.fromEntries(
68 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
69 | );
70 |
71 | addBase({
72 | ":root": newVars
73 | });
74 | }
75 |
--------------------------------------------------------------------------------
/tsconfig.app.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 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/*": ["./src/*"]
19 | },
20 |
21 | /* Linting */
22 | "strict": true,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 | "noFallthroughCasesInSwitch": true
26 | },
27 | "include": ["src"]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.node.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"root":["./vite.config.ts"],"version":"5.6.2"}
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import react from "@vitejs/plugin-react";
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | alias: {
9 | "@": path.resolve(__dirname, "./src"),
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------