├── .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 | ![Logo](image/image.png){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 | 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 |
59 |
60 | 67 |
68 |
69 | 75 |
76 |
77 | 83 |
84 |
85 |
86 |
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 |
50 | 51 |
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 | {item.name} 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 | 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 | 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 | 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 | 47 |
48 | 51 | 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 | 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 | 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 | 21 |
22 | 30 | 38 | 46 | 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 | 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 | 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 | 38 |
39 |
40 |
41 | 42 |
43 |
Position
44 |
51 |
52 | 53 |
54 | x 55 |
56 |
57 |
58 | 59 |
60 | y 61 |
62 |
63 | 64 |
65 | 72 |
73 |
74 |
75 | 76 |
77 |
Rotate
78 |
85 | 86 |
87 |
88 | 95 |
96 |
97 |
98 |
99 | ); 100 | }; 101 | 102 | export default Transform; 103 | -------------------------------------------------------------------------------- /src/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 | 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 | 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 |
4 |
5 | Presets 6 |
7 |
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 | 90 |
91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/pages/editor/menu-item/elements.tsx: -------------------------------------------------------------------------------- 1 | export const Elements = () => { 2 | return ( 3 |
4 |
5 | Shapes 6 |
7 |
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 | image 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 | 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 | 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 |
75 |
80 |
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 | 40 |
41 |
42 | 43 | 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 | image 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 |
83 |
84 |
85 | 86 |
92 | 93 | 97 | 98 |
99 |
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 | --------------------------------------------------------------------------------