From 7bf0984698e43b9aa2dd879f65951318ed410390 Mon Sep 17 00:00:00 2001 From: Maze <167211895+mazeincoding@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:20:04 +0100 Subject: codebase overhaul (#697) --- apps/transcription/README.md | 86 - apps/transcription/requirements.txt | 5 - apps/transcription/transcription.py | 143 -- apps/web/.env.example | 41 +- apps/web/.gitignore | 8 +- apps/web/Dockerfile | 65 - apps/web/drizzle.config.ts | 12 +- apps/web/migrations/0000_brainy_saracen.sql | 62 + apps/web/migrations/0000_hot_the_fallen.sql | 54 - apps/web/migrations/0001_tricky_jackpot.sql | 8 - apps/web/migrations/0002_cuddly_pretty_boy.sql | 1 - apps/web/migrations/0003_long_polaris.sql | 10 - apps/web/migrations/meta/0000_snapshot.json | 39 +- apps/web/migrations/meta/0001_snapshot.json | 344 ---- apps/web/migrations/meta/0002_snapshot.json | 345 ---- apps/web/migrations/meta/0003_snapshot.json | 365 ---- apps/web/migrations/meta/_journal.json | 27 +- apps/web/next-env.d.ts | 6 + apps/web/package.json | 33 +- apps/web/public/frame.svg | 10 - apps/web/public/logo.png | Bin 1789 -> 0 bytes apps/web/public/logo.svg | 10 - .../logos/opencut/1k/logo-black-with-text.png | Bin 0 -> 10331 bytes apps/web/public/logos/opencut/1k/logo-black.png | Bin 0 -> 2101 bytes .../logos/opencut/1k/logo-white-with-text.png | Bin 0 -> 10839 bytes apps/web/public/logos/opencut/1k/logo-white.png | Bin 0 -> 2149 bytes .../logos/opencut/2k/logo-black-with-text.png | Bin 0 -> 22647 bytes apps/web/public/logos/opencut/2k/logo-black.png | Bin 0 -> 4815 bytes .../logos/opencut/2k/logo-white-with-text.png | Bin 0 -> 22670 bytes apps/web/public/logos/opencut/2k/logo-white.png | Bin 0 -> 4153 bytes .../logos/opencut/4k/logo-black-with-text.png | Bin 0 -> 52288 bytes apps/web/public/logos/opencut/4k/logo-black.png | Bin 0 -> 11217 bytes .../logos/opencut/4k/logo-white-with-text.png | Bin 0 -> 54360 bytes apps/web/public/logos/opencut/4k/logo-white.png | Bin 0 -> 10259 bytes apps/web/public/logos/opencut/svg/logo.svg | 10 + apps/web/public/logos/others/fal.svg | 3 + apps/web/public/logos/others/vercel.svg | 3 + apps/web/src/app/(auth)/login/page.tsx | 146 -- apps/web/src/app/(auth)/signup/page.tsx | 162 -- apps/web/src/app/animation/page.tsx | 24 - apps/web/src/app/api/auth/[...all]/route.ts | 2 +- apps/web/src/app/api/get-upload-url/route.ts | 128 -- apps/web/src/app/api/health/route.ts | 6 +- apps/web/src/app/api/sounds/search/route.ts | 467 ++--- apps/web/src/app/api/transcribe/route.ts | 189 -- apps/web/src/app/api/waitlist/export/route.ts | 83 - apps/web/src/app/base-page.tsx | 53 + apps/web/src/app/blog/[slug]/page.tsx | 264 +-- apps/web/src/app/blog/page.tsx | 151 +- apps/web/src/app/contributors/page.tsx | 536 +++--- apps/web/src/app/editor/[project_id]/layout.tsx | 9 - apps/web/src/app/editor/[project_id]/page.tsx | 543 +----- apps/web/src/app/globals.css | 430 ++--- apps/web/src/app/layout.tsx | 86 +- apps/web/src/app/metadata.ts | 164 +- apps/web/src/app/page.tsx | 22 +- apps/web/src/app/privacy/page.tsx | 512 +++--- apps/web/src/app/projects/page.tsx | 1552 +++++++++------- apps/web/src/app/projects/store.ts | 129 ++ apps/web/src/app/roadmap/page.tsx | 371 ++-- apps/web/src/app/robots.ts | 18 +- apps/web/src/app/rss.xml/route.ts | 78 +- apps/web/src/app/sitemap.ts | 110 +- apps/web/src/app/sponsors/page.tsx | 82 + apps/web/src/app/terms/page.tsx | 580 +++--- apps/web/src/app/why-not-capcut/page.tsx | 191 -- apps/web/src/components/delete-project-dialog.tsx | 68 - apps/web/src/components/editable-timecode.tsx | 154 ++ apps/web/src/components/editor/audio-waveform.tsx | 162 -- .../editor/dialogs/delete-project-dialog.tsx | 84 + .../components/editor/dialogs/migration-dialog.tsx | 44 + .../editor/dialogs/project-info-dialog.tsx | 83 + .../editor/dialogs/rename-project-dialog.tsx | 72 + .../components/editor/dialogs/shortcuts-dialog.tsx | 229 +++ apps/web/src/components/editor/editor-header.tsx | 279 +-- apps/web/src/components/editor/export-button.tsx | 588 +++--- .../src/components/editor/layout-guide-overlay.tsx | 30 +- .../components/editor/media-panel/drag-overlay.tsx | 57 - .../src/components/editor/media-panel/index.tsx | 52 - .../web/src/components/editor/media-panel/store.ts | 86 - .../src/components/editor/media-panel/tabbar.tsx | 99 - .../editor/media-panel/views/captions.tsx | 303 ---- .../components/editor/media-panel/views/media.tsx | 545 ------ .../editor/media-panel/views/settings.tsx | 332 ---- .../components/editor/media-panel/views/sounds.tsx | 562 ------ .../editor/media-panel/views/stickers.tsx | 612 ------- .../components/editor/media-panel/views/text.tsx | 36 - apps/web/src/components/editor/onboarding.tsx | 227 ++- apps/web/src/components/editor/panel-base-view.tsx | 85 - .../components/editor/panel-preset-selector.tsx | 91 - .../editor/panels/assets/drag-overlay.tsx | 61 + .../editor/panels/assets/draggable-item.tsx | 244 +++ .../src/components/editor/panels/assets/index.tsx | 52 + .../src/components/editor/panels/assets/tabbar.tsx | 116 ++ .../editor/panels/assets/views/captions.tsx | 152 ++ .../editor/panels/assets/views/media.tsx | 675 +++++++ .../editor/panels/assets/views/settings.tsx | 394 ++++ .../editor/panels/assets/views/sounds.tsx | 574 ++++++ .../editor/panels/assets/views/stickers.tsx | 615 +++++++ .../components/editor/panels/assets/views/text.tsx | 46 + .../components/editor/panels/panel-base-view.tsx | 85 + .../src/components/editor/panels/preview/index.tsx | 124 ++ .../editor/panels/properties/audio-properties.tsx | 5 + .../components/editor/panels/properties/index.tsx | 68 + .../editor/panels/properties/property-item.tsx | 91 + .../editor/panels/properties/text-properties.tsx | 410 +++++ .../editor/panels/properties/video-properties.tsx | 9 + apps/web/src/components/editor/preview-panel.tsx | 1134 ------------ .../editor/properties-panel/audio-properties.tsx | 5 - .../components/editor/properties-panel/index.tsx | 70 - .../editor/properties-panel/media-properties.tsx | 5 - .../editor/properties-panel/property-item.tsx | 86 - .../editor/properties-panel/text-properties.tsx | 370 ---- apps/web/src/components/editor/scenes-view.tsx | 367 ++-- apps/web/src/components/editor/selection-box.tsx | 87 +- apps/web/src/components/editor/snap-indicator.tsx | 77 - apps/web/src/components/editor/speed-control.tsx | 46 - .../components/editor/timeline/audio-waveform.tsx | 176 ++ .../src/components/editor/timeline/bookmarks.tsx | 111 ++ .../src/components/editor/timeline/drag-line.tsx | 28 + apps/web/src/components/editor/timeline/index.tsx | 1421 ++++++--------- .../components/editor/timeline/snap-indicator.tsx | 52 + .../editor/timeline/timeline-cache-indicator.tsx | 126 -- .../editor/timeline/timeline-element.tsx | 923 ++++++---- .../components/editor/timeline/timeline-marker.tsx | 67 - .../editor/timeline/timeline-playhead.tsx | 244 +-- .../components/editor/timeline/timeline-ruler.tsx | 83 + .../components/editor/timeline/timeline-tick.tsx | 39 + .../editor/timeline/timeline-toolbar.tsx | 696 ++++--- .../components/editor/timeline/timeline-track.tsx | 1300 ++----------- apps/web/src/components/footer.tsx | 234 ++- .../src/components/gitHub-contribute-section.tsx | 47 + apps/web/src/components/header-base.tsx | 41 - apps/web/src/components/header.tsx | 180 +- apps/web/src/components/icons.tsx | 233 --- .../web/src/components/keyboard-shortcuts-help.tsx | 245 --- apps/web/src/components/landing/handlebars.tsx | 258 ++- apps/web/src/components/landing/hero.tsx | 103 +- apps/web/src/components/language-select.tsx | 204 --- .../src/components/providers/editor-provider.tsx | 169 +- .../providers/migrators/scenes-migrator.tsx | 177 -- apps/web/src/components/rename-project-dialog.tsx | 73 - apps/web/src/components/storage-provider.tsx | 111 +- apps/web/src/components/theme-toggle.tsx | 41 +- apps/web/src/components/ui/accordion.tsx | 64 +- apps/web/src/components/ui/alert-dialog.tsx | 172 +- apps/web/src/components/ui/alert.tsx | 83 +- apps/web/src/components/ui/audio-player.tsx | 127 -- apps/web/src/components/ui/avatar.tsx | 56 +- apps/web/src/components/ui/badge.tsx | 48 +- apps/web/src/components/ui/breadcrumb.tsx | 146 +- apps/web/src/components/ui/button.tsx | 94 +- apps/web/src/components/ui/calendar.tsx | 114 +- apps/web/src/components/ui/card.tsx | 90 +- apps/web/src/components/ui/carousel.tsx | 388 ++-- apps/web/src/components/ui/chart.tsx | 628 +++---- apps/web/src/components/ui/checkbox.tsx | 34 +- apps/web/src/components/ui/color-picker.tsx | 637 +++---- apps/web/src/components/ui/command.tsx | 191 +- apps/web/src/components/ui/context-menu.tsx | 323 ++-- apps/web/src/components/ui/dialog.tsx | 166 +- apps/web/src/components/ui/draggable-item.tsx | 247 --- apps/web/src/components/ui/drawer.tsx | 138 +- apps/web/src/components/ui/dropdown-menu.tsx | 326 ++-- apps/web/src/components/ui/editable-timecode.tsx | 138 -- apps/web/src/components/ui/font-picker.tsx | 64 +- apps/web/src/components/ui/form.tsx | 234 +-- apps/web/src/components/ui/hover-card.tsx | 26 +- .../src/components/ui/image-timeline-treatment.tsx | 102 -- apps/web/src/components/ui/input-otp.tsx | 92 +- apps/web/src/components/ui/input-with-back.tsx | 142 +- apps/web/src/components/ui/input.tsx | 225 ++- apps/web/src/components/ui/label.tsx | 20 +- apps/web/src/components/ui/menubar.tsx | 324 ++-- apps/web/src/components/ui/navigation-menu.tsx | 166 +- apps/web/src/components/ui/pagination.tsx | 148 +- apps/web/src/components/ui/phone-input.tsx | 246 +-- apps/web/src/components/ui/popover.tsx | 30 +- apps/web/src/components/ui/progress.tsx | 32 +- apps/web/src/components/ui/prose.tsx | 26 +- apps/web/src/components/ui/radio-group.tsx | 52 +- .../src/components/ui/react-markdown-wrapper.tsx | 26 + apps/web/src/components/ui/resizable.tsx | 42 +- apps/web/src/components/ui/scroll-area.tsx | 20 +- apps/web/src/components/ui/select.tsx | 249 +-- apps/web/src/components/ui/separator.tsx | 38 +- apps/web/src/components/ui/sheet.tsx | 182 +- apps/web/src/components/ui/sidebar.tsx | 1440 +++++++-------- apps/web/src/components/ui/skeleton.tsx | 18 +- apps/web/src/components/ui/slider.tsx | 32 +- apps/web/src/components/ui/sonner.tsx | 46 +- apps/web/src/components/ui/spinner.tsx | 17 + apps/web/src/components/ui/split-button.tsx | 120 +- apps/web/src/components/ui/sponsor-button.tsx | 39 - apps/web/src/components/ui/switch.tsx | 34 +- apps/web/src/components/ui/table.tsx | 150 +- apps/web/src/components/ui/tabs.tsx | 62 +- apps/web/src/components/ui/textarea.tsx | 30 +- apps/web/src/components/ui/toast.tsx | 160 +- apps/web/src/components/ui/toaster.tsx | 31 - apps/web/src/components/ui/toggle-group.tsx | 74 +- apps/web/src/components/ui/toggle.tsx | 56 +- apps/web/src/components/ui/tooltip.tsx | 111 +- apps/web/src/components/ui/video-player.tsx | 133 -- apps/web/src/constants/actions.ts | 295 --- apps/web/src/constants/editor-constants.ts | 15 + apps/web/src/constants/export-constants.ts | 12 + apps/web/src/constants/font-constants.ts | 122 +- apps/web/src/constants/language-constants.ts | 11 + apps/web/src/constants/project-constants.ts | 27 + apps/web/src/constants/site-constants.ts | 66 + apps/web/src/constants/site.ts | 33 - apps/web/src/constants/stickers-constants.ts | 16 + apps/web/src/constants/text-constants.ts | 50 +- apps/web/src/constants/timeline-constants.ts | 107 -- apps/web/src/constants/timeline-constants.tsx | 72 + apps/web/src/constants/transcription-constants.ts | 58 + apps/web/src/core/index.ts | 50 + apps/web/src/core/managers/audio-manager.ts | 335 ++++ apps/web/src/core/managers/commands.ts | 44 + apps/web/src/core/managers/media-manager.ts | 162 ++ apps/web/src/core/managers/playback-manager.ts | 162 ++ apps/web/src/core/managers/project-manager.ts | 648 +++++++ apps/web/src/core/managers/renderer-manager.ts | 129 ++ apps/web/src/core/managers/save-manager.ts | 107 ++ apps/web/src/core/managers/scenes-manager.ts | 316 ++++ apps/web/src/core/managers/selection-manager.ts | 35 + apps/web/src/core/managers/timeline-manager.ts | 275 +++ apps/web/src/data/colors/pattern-craft.ts | 76 +- apps/web/src/data/colors/solid.ts | 484 ++--- apps/web/src/data/colors/syntax-ui.tsx | 42 +- apps/web/src/env.ts | 45 - apps/web/src/hooks/actions/use-action-handler.ts | 52 + apps/web/src/hooks/actions/use-editor-actions.ts | 312 ++++ apps/web/src/hooks/auth/useLogin.ts | 60 - apps/web/src/hooks/auth/useSignUp.ts | 65 - apps/web/src/hooks/storage/use-local-storage.ts | 58 + .../timeline/element/use-element-interaction.ts | 619 +++++++ .../hooks/timeline/element/use-element-resize.ts | 331 ++++ .../timeline/element/use-element-selection.ts | 114 ++ .../web/src/hooks/timeline/use-edge-auto-scroll.ts | 100 + apps/web/src/hooks/timeline/use-scroll-sync.ts | 173 ++ apps/web/src/hooks/timeline/use-selection-box.ts | 243 +++ .../hooks/timeline/use-snap-indicator-position.ts | 62 + .../src/hooks/timeline/use-timeline-drag-drop.ts | 460 +++++ .../src/hooks/timeline/use-timeline-playhead.ts | 218 +++ apps/web/src/hooks/timeline/use-timeline-seek.ts | 197 ++ .../src/hooks/timeline/use-timeline-snapping.ts | 159 ++ apps/web/src/hooks/timeline/use-timeline-zoom.ts | 224 +++ apps/web/src/hooks/use-aspect-ratio.ts | 97 - apps/web/src/hooks/use-drag-drop.ts | 85 - apps/web/src/hooks/use-edge-auto-scroll.ts | 101 -- apps/web/src/hooks/use-editor-actions.ts | 241 --- apps/web/src/hooks/use-editor.ts | 39 + apps/web/src/hooks/use-file-upload.ts | 103 ++ apps/web/src/hooks/use-frame-cache.ts | 348 ---- apps/web/src/hooks/use-highlight-scroll.ts | 36 - apps/web/src/hooks/use-infinite-scroll.ts | 46 +- apps/web/src/hooks/use-keybinding-conflicts.ts | 59 - apps/web/src/hooks/use-keybindings.ts | 102 +- apps/web/src/hooks/use-keyboard-shortcuts-help.ts | 189 +- apps/web/src/hooks/use-mobile.ts | 19 + apps/web/src/hooks/use-mobile.tsx | 21 - apps/web/src/hooks/use-playback-controls.ts | 109 -- apps/web/src/hooks/use-raf-loop.ts | 20 + apps/web/src/hooks/use-reveal-item.ts | 36 + apps/web/src/hooks/use-selection-box.ts | 215 --- apps/web/src/hooks/use-sound-search.ts | 302 ++- apps/web/src/hooks/use-timeline-element-resize.ts | 257 --- apps/web/src/hooks/use-timeline-playhead.ts | 218 --- apps/web/src/hooks/use-timeline-snapping.ts | 178 -- apps/web/src/hooks/use-timeline-zoom.ts | 60 - apps/web/src/hooks/use-toast.ts | 191 -- apps/web/src/lib/actions/definitions.ts | 162 ++ apps/web/src/lib/actions/index.ts | 3 + apps/web/src/lib/actions/registry.ts | 61 + apps/web/src/lib/actions/types.ts | 42 + apps/web/src/lib/auth/client.ts | 6 + apps/web/src/lib/auth/server.ts | 43 + apps/web/src/lib/blog-query.ts | 64 - apps/web/src/lib/blog/query.ts | 72 + apps/web/src/lib/canvas-gradients.ts | 219 --- apps/web/src/lib/commands/base-command.ts | 11 + apps/web/src/lib/commands/index.ts | 6 + apps/web/src/lib/commands/media/add-media-asset.ts | 61 + apps/web/src/lib/commands/media/index.ts | 2 + .../src/lib/commands/media/remove-media-asset.ts | 85 + apps/web/src/lib/commands/project/index.ts | 1 + .../commands/project/update-project-settings.ts | 46 + apps/web/src/lib/commands/scene/create-scene.ts | 40 + apps/web/src/lib/commands/scene/delete-scene.ts | 59 + apps/web/src/lib/commands/scene/index.ts | 5 + apps/web/src/lib/commands/scene/remove-bookmark.ts | 59 + apps/web/src/lib/commands/scene/rename-scene.ts | 46 + apps/web/src/lib/commands/scene/toggle-bookmark.ts | 52 + .../src/lib/commands/timeline/clipboard/index.ts | 1 + .../src/lib/commands/timeline/clipboard/paste.ts | 263 +++ .../commands/timeline/element/delete-elements.ts | 48 + .../timeline/element/duplicate-elements.ts | 114 ++ .../web/src/lib/commands/timeline/element/index.ts | 11 + .../commands/timeline/element/insert-element.ts | 311 ++++ .../lib/commands/timeline/element/move-elements.ts | 127 ++ .../commands/timeline/element/split-elements.ts | 133 ++ .../timeline/element/toggle-elements-muted.ts | 57 + .../timeline/element/toggle-elements-visibility.ts | 47 + .../timeline/element/update-element-duration.ts | 37 + .../timeline/element/update-element-start-time.ts | 48 + .../timeline/element/update-element-trim.ts | 48 + .../timeline/element/update-text-element.ts | 52 + apps/web/src/lib/commands/timeline/index.ts | 3 + .../src/lib/commands/timeline/track/add-track.ts | 53 + apps/web/src/lib/commands/timeline/track/index.ts | 4 + .../lib/commands/timeline/track/remove-track.ts | 35 + .../commands/timeline/track/toggle-track-mute.ts | 39 + .../timeline/track/toggle-track-visibility.ts | 40 + apps/web/src/lib/db/index.ts | 19 + apps/web/src/lib/db/schema.ts | 59 + apps/web/src/lib/drag-data.ts | 48 + apps/web/src/lib/editor-utils.ts | 46 - apps/web/src/lib/export.ts | 313 +--- apps/web/src/lib/font-config.ts | 39 - apps/web/src/lib/gradients/canvas.ts | 923 ++++++++++ apps/web/src/lib/gradients/index.ts | 2 + apps/web/src/lib/gradients/parser.ts | 652 +++++++ apps/web/src/lib/iconify-api.ts | 367 ++-- apps/web/src/lib/media-processing.ts | 89 - apps/web/src/lib/media/audio.ts | 459 +++++ apps/web/src/lib/media/media-utils.ts | 32 + apps/web/src/lib/media/mediabunny.ts | 226 +++ apps/web/src/lib/media/processing.ts | 282 +++ apps/web/src/lib/mediabunny-utils.ts | 311 ---- apps/web/src/lib/rate-limit.ts | 21 +- apps/web/src/lib/scenes.ts | 103 ++ apps/web/src/lib/schemas/waitlist.ts | 15 - apps/web/src/lib/storage/indexeddb-adapter.ts | 89 - apps/web/src/lib/storage/opfs-adapter.ts | 73 - apps/web/src/lib/storage/storage-service.ts | 480 ----- apps/web/src/lib/storage/types.ts | 69 - apps/web/src/lib/time.ts | 357 ++-- apps/web/src/lib/timeline-renderer.ts | 279 --- apps/web/src/lib/timeline.ts | 54 - apps/web/src/lib/timeline/bookmarks.ts | 61 + apps/web/src/lib/timeline/drop-utils.ts | 234 +++ apps/web/src/lib/timeline/element-utils.ts | 304 ++++ apps/web/src/lib/timeline/index.ts | 23 + apps/web/src/lib/timeline/ruler-utils.ts | 228 +++ apps/web/src/lib/timeline/track-utils.ts | 244 +++ apps/web/src/lib/timeline/zoom-utils.ts | 53 + apps/web/src/lib/transcription-utils.ts | 13 - apps/web/src/lib/transcription/caption.ts | 49 + apps/web/src/lib/utils.ts | 80 - apps/web/src/lib/video-cache.ts | 186 -- apps/web/src/lib/zk-encryption.ts | 71 - apps/web/src/middleware.ts | 30 +- apps/web/src/services/media/video-cache.ts | 314 ++++ apps/web/src/services/renderer/canvas-renderer.ts | 87 + apps/web/src/services/renderer/nodes/base-node.ts | 35 + .../renderer/nodes/blur-background-node.ts | 75 + apps/web/src/services/renderer/nodes/color-node.ts | 31 + apps/web/src/services/renderer/nodes/image-node.ts | 93 + apps/web/src/services/renderer/nodes/root-node.ts | 11 + .../src/services/renderer/nodes/sticker-node.ts | 87 + apps/web/src/services/renderer/nodes/text-node.ts | 74 + apps/web/src/services/renderer/nodes/video-node.ts | 84 + apps/web/src/services/renderer/scene-builder.ts | 130 ++ apps/web/src/services/renderer/scene-exporter.ts | 152 ++ apps/web/src/services/storage/indexeddb-adapter.ts | 101 ++ apps/web/src/services/storage/migrations/base.ts | 5 + apps/web/src/services/storage/migrations/index.ts | 14 + apps/web/src/services/storage/migrations/runner.ts | 134 ++ .../src/services/storage/migrations/v0-to-v1.ts | 93 + .../src/services/storage/migrations/v1-to-v2.ts | 339 ++++ .../src/services/storage/migrations/v2-to-v3.ts | 92 + .../services/storage/migrations/version-manager.ts | 73 + apps/web/src/services/storage/opfs-adapter.ts | 73 + apps/web/src/services/storage/storage-service.ts | 475 +++++ apps/web/src/services/storage/types.ts | 66 + apps/web/src/services/transcription/index.ts | 186 ++ apps/web/src/services/transcription/worker.ts | 176 ++ apps/web/src/stores/assets-panel-store.tsx | 107 ++ apps/web/src/stores/editor-store.ts | 129 +- apps/web/src/stores/keybindings-store.ts | 475 +++-- .../web/src/stores/keybindings/migrations/index.ts | 28 + .../src/stores/keybindings/migrations/v2-to-v3.ts | 26 + apps/web/src/stores/media-store.ts | 296 --- apps/web/src/stores/panel-store.ts | 300 +-- apps/web/src/stores/playback-store.ts | 174 -- apps/web/src/stores/project-store.ts | 571 ------ apps/web/src/stores/scene-store.ts | 318 ---- apps/web/src/stores/sounds-store.ts | 525 +++--- apps/web/src/stores/stickers-store.ts | 383 ++-- apps/web/src/stores/text-properties-store.ts | 30 +- apps/web/src/stores/timeline-store.ts | 1913 +------------------- apps/web/src/types/assets.ts | 9 + apps/web/src/types/blog.ts | 92 + apps/web/src/types/drag.ts | 21 + apps/web/src/types/editor.ts | 29 +- apps/web/src/types/export.ts | 33 +- apps/web/src/types/keybinding.ts | 124 +- apps/web/src/types/language.ts | 4 + apps/web/src/types/media.ts | 17 - apps/web/src/types/playback.ts | 21 - apps/web/src/types/post.ts | 92 - apps/web/src/types/project.ts | 74 +- apps/web/src/types/sounds.ts | 62 +- apps/web/src/types/stickers.ts | 3 + apps/web/src/types/time.ts | 1 + apps/web/src/types/timeline.ts | 330 ++-- apps/web/src/types/transcription.ts | 47 + apps/web/src/utils/browser.ts | 17 + apps/web/src/utils/date.ts | 7 + apps/web/src/utils/geometry.ts | 13 + apps/web/src/utils/id.ts | 28 + apps/web/src/utils/math.ts | 11 + apps/web/src/utils/platform.ts | 11 + apps/web/src/utils/string.ts | 7 + apps/web/src/utils/ui.ts | 6 + apps/web/tsconfig.json | 7 +- apps/web/tsconfig.tsbuildinfo | 2 +- 419 files changed, 33697 insertions(+), 31815 deletions(-) delete mode 100644 apps/transcription/README.md delete mode 100644 apps/transcription/requirements.txt delete mode 100644 apps/transcription/transcription.py delete mode 100644 apps/web/Dockerfile create mode 100644 apps/web/migrations/0000_brainy_saracen.sql delete mode 100644 apps/web/migrations/0000_hot_the_fallen.sql delete mode 100644 apps/web/migrations/0001_tricky_jackpot.sql delete mode 100644 apps/web/migrations/0002_cuddly_pretty_boy.sql delete mode 100644 apps/web/migrations/0003_long_polaris.sql delete mode 100644 apps/web/migrations/meta/0001_snapshot.json delete mode 100644 apps/web/migrations/meta/0002_snapshot.json delete mode 100644 apps/web/migrations/meta/0003_snapshot.json create mode 100644 apps/web/next-env.d.ts delete mode 100644 apps/web/public/frame.svg delete mode 100644 apps/web/public/logo.png delete mode 100644 apps/web/public/logo.svg create mode 100644 apps/web/public/logos/opencut/1k/logo-black-with-text.png create mode 100644 apps/web/public/logos/opencut/1k/logo-black.png create mode 100644 apps/web/public/logos/opencut/1k/logo-white-with-text.png create mode 100644 apps/web/public/logos/opencut/1k/logo-white.png create mode 100644 apps/web/public/logos/opencut/2k/logo-black-with-text.png create mode 100644 apps/web/public/logos/opencut/2k/logo-black.png create mode 100644 apps/web/public/logos/opencut/2k/logo-white-with-text.png create mode 100644 apps/web/public/logos/opencut/2k/logo-white.png create mode 100644 apps/web/public/logos/opencut/4k/logo-black-with-text.png create mode 100644 apps/web/public/logos/opencut/4k/logo-black.png create mode 100644 apps/web/public/logos/opencut/4k/logo-white-with-text.png create mode 100644 apps/web/public/logos/opencut/4k/logo-white.png create mode 100644 apps/web/public/logos/opencut/svg/logo.svg create mode 100644 apps/web/public/logos/others/fal.svg create mode 100644 apps/web/public/logos/others/vercel.svg delete mode 100644 apps/web/src/app/(auth)/login/page.tsx delete mode 100644 apps/web/src/app/(auth)/signup/page.tsx delete mode 100644 apps/web/src/app/animation/page.tsx delete mode 100644 apps/web/src/app/api/get-upload-url/route.ts delete mode 100644 apps/web/src/app/api/transcribe/route.ts delete mode 100644 apps/web/src/app/api/waitlist/export/route.ts create mode 100644 apps/web/src/app/base-page.tsx delete mode 100644 apps/web/src/app/editor/[project_id]/layout.tsx create mode 100644 apps/web/src/app/projects/store.ts create mode 100644 apps/web/src/app/sponsors/page.tsx delete mode 100644 apps/web/src/app/why-not-capcut/page.tsx delete mode 100644 apps/web/src/components/delete-project-dialog.tsx create mode 100644 apps/web/src/components/editable-timecode.tsx delete mode 100644 apps/web/src/components/editor/audio-waveform.tsx create mode 100644 apps/web/src/components/editor/dialogs/delete-project-dialog.tsx create mode 100644 apps/web/src/components/editor/dialogs/migration-dialog.tsx create mode 100644 apps/web/src/components/editor/dialogs/project-info-dialog.tsx create mode 100644 apps/web/src/components/editor/dialogs/rename-project-dialog.tsx create mode 100644 apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx delete mode 100644 apps/web/src/components/editor/media-panel/drag-overlay.tsx delete mode 100644 apps/web/src/components/editor/media-panel/index.tsx delete mode 100644 apps/web/src/components/editor/media-panel/store.ts delete mode 100644 apps/web/src/components/editor/media-panel/tabbar.tsx delete mode 100644 apps/web/src/components/editor/media-panel/views/captions.tsx delete mode 100644 apps/web/src/components/editor/media-panel/views/media.tsx delete mode 100644 apps/web/src/components/editor/media-panel/views/settings.tsx delete mode 100644 apps/web/src/components/editor/media-panel/views/sounds.tsx delete mode 100644 apps/web/src/components/editor/media-panel/views/stickers.tsx delete mode 100644 apps/web/src/components/editor/media-panel/views/text.tsx delete mode 100644 apps/web/src/components/editor/panel-base-view.tsx delete mode 100644 apps/web/src/components/editor/panel-preset-selector.tsx create mode 100644 apps/web/src/components/editor/panels/assets/drag-overlay.tsx create mode 100644 apps/web/src/components/editor/panels/assets/draggable-item.tsx create mode 100644 apps/web/src/components/editor/panels/assets/index.tsx create mode 100644 apps/web/src/components/editor/panels/assets/tabbar.tsx create mode 100644 apps/web/src/components/editor/panels/assets/views/captions.tsx create mode 100644 apps/web/src/components/editor/panels/assets/views/media.tsx create mode 100644 apps/web/src/components/editor/panels/assets/views/settings.tsx create mode 100644 apps/web/src/components/editor/panels/assets/views/sounds.tsx create mode 100644 apps/web/src/components/editor/panels/assets/views/stickers.tsx create mode 100644 apps/web/src/components/editor/panels/assets/views/text.tsx create mode 100644 apps/web/src/components/editor/panels/panel-base-view.tsx create mode 100644 apps/web/src/components/editor/panels/preview/index.tsx create mode 100644 apps/web/src/components/editor/panels/properties/audio-properties.tsx create mode 100644 apps/web/src/components/editor/panels/properties/index.tsx create mode 100644 apps/web/src/components/editor/panels/properties/property-item.tsx create mode 100644 apps/web/src/components/editor/panels/properties/text-properties.tsx create mode 100644 apps/web/src/components/editor/panels/properties/video-properties.tsx delete mode 100644 apps/web/src/components/editor/preview-panel.tsx delete mode 100644 apps/web/src/components/editor/properties-panel/audio-properties.tsx delete mode 100644 apps/web/src/components/editor/properties-panel/index.tsx delete mode 100644 apps/web/src/components/editor/properties-panel/media-properties.tsx delete mode 100644 apps/web/src/components/editor/properties-panel/property-item.tsx delete mode 100644 apps/web/src/components/editor/properties-panel/text-properties.tsx delete mode 100644 apps/web/src/components/editor/snap-indicator.tsx delete mode 100644 apps/web/src/components/editor/speed-control.tsx create mode 100644 apps/web/src/components/editor/timeline/audio-waveform.tsx create mode 100644 apps/web/src/components/editor/timeline/bookmarks.tsx create mode 100644 apps/web/src/components/editor/timeline/drag-line.tsx create mode 100644 apps/web/src/components/editor/timeline/snap-indicator.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-cache-indicator.tsx delete mode 100644 apps/web/src/components/editor/timeline/timeline-marker.tsx create mode 100644 apps/web/src/components/editor/timeline/timeline-ruler.tsx create mode 100644 apps/web/src/components/editor/timeline/timeline-tick.tsx create mode 100644 apps/web/src/components/gitHub-contribute-section.tsx delete mode 100644 apps/web/src/components/header-base.tsx delete mode 100644 apps/web/src/components/icons.tsx delete mode 100644 apps/web/src/components/keyboard-shortcuts-help.tsx delete mode 100644 apps/web/src/components/language-select.tsx delete mode 100644 apps/web/src/components/providers/migrators/scenes-migrator.tsx delete mode 100644 apps/web/src/components/rename-project-dialog.tsx delete mode 100644 apps/web/src/components/ui/audio-player.tsx delete mode 100644 apps/web/src/components/ui/draggable-item.tsx delete mode 100644 apps/web/src/components/ui/editable-timecode.tsx delete mode 100644 apps/web/src/components/ui/image-timeline-treatment.tsx create mode 100644 apps/web/src/components/ui/react-markdown-wrapper.tsx create mode 100644 apps/web/src/components/ui/spinner.tsx delete mode 100644 apps/web/src/components/ui/sponsor-button.tsx delete mode 100644 apps/web/src/components/ui/toaster.tsx delete mode 100644 apps/web/src/components/ui/video-player.tsx delete mode 100644 apps/web/src/constants/actions.ts create mode 100644 apps/web/src/constants/editor-constants.ts create mode 100644 apps/web/src/constants/export-constants.ts create mode 100644 apps/web/src/constants/language-constants.ts create mode 100644 apps/web/src/constants/project-constants.ts create mode 100644 apps/web/src/constants/site-constants.ts delete mode 100644 apps/web/src/constants/site.ts create mode 100644 apps/web/src/constants/stickers-constants.ts delete mode 100644 apps/web/src/constants/timeline-constants.ts create mode 100644 apps/web/src/constants/timeline-constants.tsx create mode 100644 apps/web/src/constants/transcription-constants.ts create mode 100644 apps/web/src/core/index.ts create mode 100644 apps/web/src/core/managers/audio-manager.ts create mode 100644 apps/web/src/core/managers/commands.ts create mode 100644 apps/web/src/core/managers/media-manager.ts create mode 100644 apps/web/src/core/managers/playback-manager.ts create mode 100644 apps/web/src/core/managers/project-manager.ts create mode 100644 apps/web/src/core/managers/renderer-manager.ts create mode 100644 apps/web/src/core/managers/save-manager.ts create mode 100644 apps/web/src/core/managers/scenes-manager.ts create mode 100644 apps/web/src/core/managers/selection-manager.ts create mode 100644 apps/web/src/core/managers/timeline-manager.ts delete mode 100644 apps/web/src/env.ts create mode 100644 apps/web/src/hooks/actions/use-action-handler.ts create mode 100644 apps/web/src/hooks/actions/use-editor-actions.ts delete mode 100644 apps/web/src/hooks/auth/useLogin.ts delete mode 100644 apps/web/src/hooks/auth/useSignUp.ts create mode 100644 apps/web/src/hooks/storage/use-local-storage.ts create mode 100644 apps/web/src/hooks/timeline/element/use-element-interaction.ts create mode 100644 apps/web/src/hooks/timeline/element/use-element-resize.ts create mode 100644 apps/web/src/hooks/timeline/element/use-element-selection.ts create mode 100644 apps/web/src/hooks/timeline/use-edge-auto-scroll.ts create mode 100644 apps/web/src/hooks/timeline/use-scroll-sync.ts create mode 100644 apps/web/src/hooks/timeline/use-selection-box.ts create mode 100644 apps/web/src/hooks/timeline/use-snap-indicator-position.ts create mode 100644 apps/web/src/hooks/timeline/use-timeline-drag-drop.ts create mode 100644 apps/web/src/hooks/timeline/use-timeline-playhead.ts create mode 100644 apps/web/src/hooks/timeline/use-timeline-seek.ts create mode 100644 apps/web/src/hooks/timeline/use-timeline-snapping.ts create mode 100644 apps/web/src/hooks/timeline/use-timeline-zoom.ts delete mode 100644 apps/web/src/hooks/use-aspect-ratio.ts delete mode 100644 apps/web/src/hooks/use-drag-drop.ts delete mode 100644 apps/web/src/hooks/use-edge-auto-scroll.ts delete mode 100644 apps/web/src/hooks/use-editor-actions.ts create mode 100644 apps/web/src/hooks/use-editor.ts create mode 100644 apps/web/src/hooks/use-file-upload.ts delete mode 100644 apps/web/src/hooks/use-frame-cache.ts delete mode 100644 apps/web/src/hooks/use-highlight-scroll.ts delete mode 100644 apps/web/src/hooks/use-keybinding-conflicts.ts create mode 100644 apps/web/src/hooks/use-mobile.ts delete mode 100644 apps/web/src/hooks/use-mobile.tsx delete mode 100644 apps/web/src/hooks/use-playback-controls.ts create mode 100644 apps/web/src/hooks/use-raf-loop.ts create mode 100644 apps/web/src/hooks/use-reveal-item.ts delete mode 100644 apps/web/src/hooks/use-selection-box.ts delete mode 100644 apps/web/src/hooks/use-timeline-element-resize.ts delete mode 100644 apps/web/src/hooks/use-timeline-playhead.ts delete mode 100644 apps/web/src/hooks/use-timeline-snapping.ts delete mode 100644 apps/web/src/hooks/use-timeline-zoom.ts delete mode 100644 apps/web/src/hooks/use-toast.ts create mode 100644 apps/web/src/lib/actions/definitions.ts create mode 100644 apps/web/src/lib/actions/index.ts create mode 100644 apps/web/src/lib/actions/registry.ts create mode 100644 apps/web/src/lib/actions/types.ts create mode 100644 apps/web/src/lib/auth/client.ts create mode 100644 apps/web/src/lib/auth/server.ts delete mode 100644 apps/web/src/lib/blog-query.ts create mode 100644 apps/web/src/lib/blog/query.ts delete mode 100644 apps/web/src/lib/canvas-gradients.ts create mode 100644 apps/web/src/lib/commands/base-command.ts create mode 100644 apps/web/src/lib/commands/index.ts create mode 100644 apps/web/src/lib/commands/media/add-media-asset.ts create mode 100644 apps/web/src/lib/commands/media/index.ts create mode 100644 apps/web/src/lib/commands/media/remove-media-asset.ts create mode 100644 apps/web/src/lib/commands/project/index.ts create mode 100644 apps/web/src/lib/commands/project/update-project-settings.ts create mode 100644 apps/web/src/lib/commands/scene/create-scene.ts create mode 100644 apps/web/src/lib/commands/scene/delete-scene.ts create mode 100644 apps/web/src/lib/commands/scene/index.ts create mode 100644 apps/web/src/lib/commands/scene/remove-bookmark.ts create mode 100644 apps/web/src/lib/commands/scene/rename-scene.ts create mode 100644 apps/web/src/lib/commands/scene/toggle-bookmark.ts create mode 100644 apps/web/src/lib/commands/timeline/clipboard/index.ts create mode 100644 apps/web/src/lib/commands/timeline/clipboard/paste.ts create mode 100644 apps/web/src/lib/commands/timeline/element/delete-elements.ts create mode 100644 apps/web/src/lib/commands/timeline/element/duplicate-elements.ts create mode 100644 apps/web/src/lib/commands/timeline/element/index.ts create mode 100644 apps/web/src/lib/commands/timeline/element/insert-element.ts create mode 100644 apps/web/src/lib/commands/timeline/element/move-elements.ts create mode 100644 apps/web/src/lib/commands/timeline/element/split-elements.ts create mode 100644 apps/web/src/lib/commands/timeline/element/toggle-elements-muted.ts create mode 100644 apps/web/src/lib/commands/timeline/element/toggle-elements-visibility.ts create mode 100644 apps/web/src/lib/commands/timeline/element/update-element-duration.ts create mode 100644 apps/web/src/lib/commands/timeline/element/update-element-start-time.ts create mode 100644 apps/web/src/lib/commands/timeline/element/update-element-trim.ts create mode 100644 apps/web/src/lib/commands/timeline/element/update-text-element.ts create mode 100644 apps/web/src/lib/commands/timeline/index.ts create mode 100644 apps/web/src/lib/commands/timeline/track/add-track.ts create mode 100644 apps/web/src/lib/commands/timeline/track/index.ts create mode 100644 apps/web/src/lib/commands/timeline/track/remove-track.ts create mode 100644 apps/web/src/lib/commands/timeline/track/toggle-track-mute.ts create mode 100644 apps/web/src/lib/commands/timeline/track/toggle-track-visibility.ts create mode 100644 apps/web/src/lib/db/index.ts create mode 100644 apps/web/src/lib/db/schema.ts create mode 100644 apps/web/src/lib/drag-data.ts delete mode 100644 apps/web/src/lib/editor-utils.ts delete mode 100644 apps/web/src/lib/font-config.ts create mode 100644 apps/web/src/lib/gradients/canvas.ts create mode 100644 apps/web/src/lib/gradients/index.ts create mode 100644 apps/web/src/lib/gradients/parser.ts delete mode 100644 apps/web/src/lib/media-processing.ts create mode 100644 apps/web/src/lib/media/audio.ts create mode 100644 apps/web/src/lib/media/media-utils.ts create mode 100644 apps/web/src/lib/media/mediabunny.ts create mode 100644 apps/web/src/lib/media/processing.ts delete mode 100644 apps/web/src/lib/mediabunny-utils.ts create mode 100644 apps/web/src/lib/scenes.ts delete mode 100644 apps/web/src/lib/schemas/waitlist.ts delete mode 100644 apps/web/src/lib/storage/indexeddb-adapter.ts delete mode 100644 apps/web/src/lib/storage/opfs-adapter.ts delete mode 100644 apps/web/src/lib/storage/storage-service.ts delete mode 100644 apps/web/src/lib/storage/types.ts delete mode 100644 apps/web/src/lib/timeline-renderer.ts delete mode 100644 apps/web/src/lib/timeline.ts create mode 100644 apps/web/src/lib/timeline/bookmarks.ts create mode 100644 apps/web/src/lib/timeline/drop-utils.ts create mode 100644 apps/web/src/lib/timeline/element-utils.ts create mode 100644 apps/web/src/lib/timeline/index.ts create mode 100644 apps/web/src/lib/timeline/ruler-utils.ts create mode 100644 apps/web/src/lib/timeline/track-utils.ts create mode 100644 apps/web/src/lib/timeline/zoom-utils.ts delete mode 100644 apps/web/src/lib/transcription-utils.ts create mode 100644 apps/web/src/lib/transcription/caption.ts delete mode 100644 apps/web/src/lib/utils.ts delete mode 100644 apps/web/src/lib/video-cache.ts delete mode 100644 apps/web/src/lib/zk-encryption.ts create mode 100644 apps/web/src/services/media/video-cache.ts create mode 100644 apps/web/src/services/renderer/canvas-renderer.ts create mode 100644 apps/web/src/services/renderer/nodes/base-node.ts create mode 100644 apps/web/src/services/renderer/nodes/blur-background-node.ts create mode 100644 apps/web/src/services/renderer/nodes/color-node.ts create mode 100644 apps/web/src/services/renderer/nodes/image-node.ts create mode 100644 apps/web/src/services/renderer/nodes/root-node.ts create mode 100644 apps/web/src/services/renderer/nodes/sticker-node.ts create mode 100644 apps/web/src/services/renderer/nodes/text-node.ts create mode 100644 apps/web/src/services/renderer/nodes/video-node.ts create mode 100644 apps/web/src/services/renderer/scene-builder.ts create mode 100644 apps/web/src/services/renderer/scene-exporter.ts create mode 100644 apps/web/src/services/storage/indexeddb-adapter.ts create mode 100644 apps/web/src/services/storage/migrations/base.ts create mode 100644 apps/web/src/services/storage/migrations/index.ts create mode 100644 apps/web/src/services/storage/migrations/runner.ts create mode 100644 apps/web/src/services/storage/migrations/v0-to-v1.ts create mode 100644 apps/web/src/services/storage/migrations/v1-to-v2.ts create mode 100644 apps/web/src/services/storage/migrations/v2-to-v3.ts create mode 100644 apps/web/src/services/storage/migrations/version-manager.ts create mode 100644 apps/web/src/services/storage/opfs-adapter.ts create mode 100644 apps/web/src/services/storage/storage-service.ts create mode 100644 apps/web/src/services/storage/types.ts create mode 100644 apps/web/src/services/transcription/index.ts create mode 100644 apps/web/src/services/transcription/worker.ts create mode 100644 apps/web/src/stores/assets-panel-store.tsx create mode 100644 apps/web/src/stores/keybindings/migrations/index.ts create mode 100644 apps/web/src/stores/keybindings/migrations/v2-to-v3.ts delete mode 100644 apps/web/src/stores/media-store.ts delete mode 100644 apps/web/src/stores/playback-store.ts delete mode 100644 apps/web/src/stores/project-store.ts delete mode 100644 apps/web/src/stores/scene-store.ts create mode 100644 apps/web/src/types/assets.ts create mode 100644 apps/web/src/types/blog.ts create mode 100644 apps/web/src/types/drag.ts create mode 100644 apps/web/src/types/language.ts delete mode 100644 apps/web/src/types/media.ts delete mode 100644 apps/web/src/types/playback.ts delete mode 100644 apps/web/src/types/post.ts create mode 100644 apps/web/src/types/stickers.ts create mode 100644 apps/web/src/types/time.ts create mode 100644 apps/web/src/types/transcription.ts create mode 100644 apps/web/src/utils/browser.ts create mode 100644 apps/web/src/utils/date.ts create mode 100644 apps/web/src/utils/geometry.ts create mode 100644 apps/web/src/utils/id.ts create mode 100644 apps/web/src/utils/math.ts create mode 100644 apps/web/src/utils/platform.ts create mode 100644 apps/web/src/utils/string.ts create mode 100644 apps/web/src/utils/ui.ts (limited to 'apps') diff --git a/apps/transcription/README.md b/apps/transcription/README.md deleted file mode 100644 index 07be6790..00000000 --- a/apps/transcription/README.md +++ /dev/null @@ -1,86 +0,0 @@ -Before you follow anything in this guide, please make sure you've followed the steps in the [README](../../README.md) (under "Optional: Auto-captions (Transcription) Setup"). - -Open your terminal and make sure you're in the `apps/transcription` directory. - -1. Create virtual environment - -```bash -python -m venv env -``` - -2. Activate it - -**Windows:** - -```bash -env\Scripts\activate -``` - -**macOS/Linux:** - -```bash -source env/bin/activate -``` - -> Note: if you're using VS Code/Cursor and you're seeing errors with the imports about the modules not being found, -> You might have to press CTRL + Shift + P -> Python: Select Interpreter -> Enter interpreter path -> Find -> env -> scripts -> python.exe - -3. Install libraries/packages/whatever you wanna call them - -```bash -pip install -r requirements.txt -``` - -4. Make sure you have a Modal account. If you don't: [create one](https://modal.com/) - -> If you don't know what Modal is: it allows us to process the actual audio and transcribe with Whisper by providing the infra to run Python code with a lot of RAM, generally affordable. - -5. Once you've got a Modal accoumt, run this: - -```bash -python -m modal setup -``` - -It's gonna open a browser so you can authenticate. - -6. Test it if you want to make sure it actually works: - -```bash -modal run transcription.py -``` - -6. Deploy the function! - -```bash -modal deploy transcription.py -``` - -7. Set the required secrets in Modal - -So the script we just deployed interacts with Cloudflare to do two things: - -- Download the audio (so it can be transcribed with Whisper) -- Delete the file after processing (privacy) - -To do those things, the script needs access to these environment variables: -```bash -CLOUDFLARE_ACCOUNT_ID=your-account-id -R2_ACCESS_KEY_ID=your-access-key-id -R2_SECRET_ACCESS_KEY=your-secret-access-key -R2_BUCKET_NAME=opencut-transcription -``` - -Remember, we set these earlier in `.env.local`. - -So let's do it: - - - Go to [Modal Secrets](https://modal.com/secrets/mazewinther/main) - - Click "Custom" and enter "opencut-r2-secrets" for the name. - - Now you can just click "Import .env" and copy/paste the 4 variables from your `.env.local` file. Copy and paste these only: - ```bash - CLOUDFLARE_ACCOUNT_ID=your-account-id - R2_ACCESS_KEY_ID=your-access-key-id - R2_SECRET_ACCESS_KEY=your-secret-access-key - R2_BUCKET_NAME=opencut-transcription - ``` - - Click "Done" and you should see some cool particles! \ No newline at end of file diff --git a/apps/transcription/requirements.txt b/apps/transcription/requirements.txt deleted file mode 100644 index c002e683..00000000 --- a/apps/transcription/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -modal -openai-whisper -boto3 -pydantic -cryptography \ No newline at end of file diff --git a/apps/transcription/transcription.py b/apps/transcription/transcription.py deleted file mode 100644 index ad55f6d4..00000000 --- a/apps/transcription/transcription.py +++ /dev/null @@ -1,143 +0,0 @@ -import modal -from pydantic import BaseModel - -app = modal.App("opencut-transcription") - -class TranscribeRequest(BaseModel): - filename: str - language: str = "auto" - decryptionKey: str = None - iv: str = None - -@app.function( - image=modal.Image.debian_slim() - .apt_install(["ffmpeg"]) - .pip_install(["openai-whisper", "boto3", "fastapi[standard]", "pydantic", "cryptography"]), - gpu="A10G", - timeout=300, # 5m - secrets=[modal.Secret.from_name("opencut-r2-secrets")] -) -@modal.fastapi_endpoint(method="POST") -def transcribe_audio(request: TranscribeRequest): - import whisper - import boto3 - import tempfile - import os - import json - - try: - filename = request.filename - language = request.language - decryption_key = request.decryptionKey - iv = request.iv - - if not filename: - return { - "error": "Missing filename parameter" - } - - # Initialize R2 client - s3_client = boto3.client( - 's3', - endpoint_url=f'https://{os.environ["CLOUDFLARE_ACCOUNT_ID"]}.r2.cloudflarestorage.com', - aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"], - aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"], - region_name='auto' - ) - - # Create temporary file for audio - with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file: - temp_path = temp_file.name - - try: - # Download audio from R2 - s3_client.download_file( - os.environ["R2_BUCKET_NAME"], - filename, - temp_path - ) - - # If decryption key provided, decrypt the file directly (zero-knowledge) - if decryption_key and iv: - import base64 - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - from cryptography.hazmat.backends import default_backend - - # Read the encrypted file - with open(temp_path, 'rb') as f: - encrypted_data = f.read() - - # Decode the key and IV from base64 - key_bytes = base64.b64decode(decryption_key) - iv_bytes = base64.b64decode(iv) - - # Decrypt the data using AES-GCM - # Extract the tag (last 16 bytes) and ciphertext - tag = encrypted_data[-16:] - ciphertext = encrypted_data[:-16] - - cipher = Cipher( - algorithms.AES(key_bytes), - modes.GCM(iv_bytes, tag), - backend=default_backend() - ) - decryptor = cipher.decryptor() - decrypted_data = decryptor.update(ciphertext) + decryptor.finalize() - - # Write decrypted audio back to temp file - with open(temp_path, 'wb') as f: - f.write(decrypted_data) - - # Load Whisper model - model = whisper.load_model("base") - - # Transcribe audio - if language == "auto": - result = model.transcribe(temp_path) - else: - result = model.transcribe(temp_path, language=language.lower()) - - # Delete audio file from R2 (cleanup) - s3_client.delete_object( - Bucket=os.environ["R2_BUCKET_NAME"], - Key=filename - ) - - # Adjust segment timing - Whisper is consistently late by ~500ms - adjusted_segments = [] - for segment in result["segments"]: - adjusted_segment = segment.copy() - # Shift start/end times earlier by 500ms, don't go below 0 - adjusted_segment["start"] = max(0, segment["start"] - 0.5) - adjusted_segment["end"] = max(0.5, segment["end"] - 0.5) # Ensure duration is at least 0.5s - adjusted_segments.append(adjusted_segment) - - return { - "text": result["text"], - "segments": adjusted_segments, - "language": result["language"] - } - - finally: - # Clean up temporary file - if os.path.exists(temp_path): - os.unlink(temp_path) - - except Exception as e: - import traceback - print(f"Transcription error: {str(e)}") - print(f"Traceback: {traceback.format_exc()}") - - # Return error response that matches expected format - return { - "error": str(e), - "text": "", - "segments": [], - "language": "unknown" - } - -@app.local_entrypoint() -def main(): - # Test function - you can call this with modal run transcription.py - print("Transcription service is ready to deploy!") - print("Deploy with: modal deploy transcription.py") \ No newline at end of file diff --git a/apps/web/.env.example b/apps/web/.env.example index e77d3c5a..b321f09a 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,33 +1,28 @@ -# Environment Variables Example +# Environment variables example # Copy this file to .env.local and update the values as needed -DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" +# Node +NODE_ENV=development -# Better Auth -NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000 -BETTER_AUTH_SECRET=your-secret-key-here +# Public +NEXT_PUBLIC_SITE_URL=http://localhost:3000 +NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com -# Development Environment -NODE_ENV=development +# Server +DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut" +BETTER_AUTH_SECRET=your_better_auth_secret -# Redis UPSTASH_REDIS_REST_URL=http://localhost:8079 -UPSTASH_REDIS_REST_TOKEN=example_token +UPSTASH_REDIS_REST_TOKEN=example_token_here -# Marble Blog -MARBLE_WORKSPACE_KEY=cm6ytuq9x0000i803v0isidst # example organization key -NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com +MARBLE_WORKSPACE_KEY=your_workspace_key_here -# Freesound (generate at https://freesound.org/apiv2/apply/) -FREESOUND_CLIENT_ID=... -FREESOUND_API_KEY=... +FREESOUND_CLIENT_ID=your_client_id_here +FREESOUND_API_KEY=your_api_key_here -# Cloudflare R2 (for auto-captions/transcription) -# Get these from Cloudflare Dashboard > R2 > Manage R2 API tokens -CLOUDFLARE_ACCOUNT_ID=your-account-id -R2_ACCESS_KEY_ID=your-access-key-id -R2_SECRET_ACCESS_KEY=your-secret-access-key -R2_BUCKET_NAME=opencut-transcription +CLOUDFLARE_ACCOUNT_ID=your_account_id_here +R2_ACCESS_KEY_ID=your_access_key_here +R2_SECRET_ACCESS_KEY=your_secret_key_here +R2_BUCKET_NAME=opencut-transcription # whatever you named your r2 bucket -# Modal transcription endpoint (from modal deploy transcription.py) -MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run \ No newline at end of file +MODAL_TRANSCRIPTION_URL=your_modal_url_here \ No newline at end of file diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 36b42034..dc069c57 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -1,2 +1,8 @@ # Turborepo -.turbo \ No newline at end of file +.turbo + +# Env vars +.env* +!.env.example + +.next/ \ No newline at end of file diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile deleted file mode 100644 index 6c42e708..00000000 --- a/apps/web/Dockerfile +++ /dev/null @@ -1,65 +0,0 @@ -FROM oven/bun:alpine AS base - -# Install dependencies and build the application -FROM base AS builder - -WORKDIR /app - -ARG FREESOUND_CLIENT_ID -ARG FREESOUND_API_KEY - -COPY package.json package.json -COPY bun.lock bun.lock -COPY turbo.json turbo.json - -COPY apps/web/package.json apps/web/package.json -COPY packages/db/package.json packages/db/package.json -COPY packages/auth/package.json packages/auth/package.json - -RUN bun install - -COPY apps/web/ apps/web/ -COPY packages/db/ packages/db/ -COPY packages/auth/ packages/auth/ - -ENV NODE_ENV production -ENV NEXT_TELEMETRY_DISABLED 1 -# Set build-time environment variables for validation -ENV DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" -ENV BETTER_AUTH_SECRET="build-time-secret" -ENV UPSTASH_REDIS_REST_URL="http://localhost:8079" -ENV UPSTASH_REDIS_REST_TOKEN="example_token" -ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000" - -ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID -ENV FREESOUND_API_KEY=$FREESOUND_API_KEY - -WORKDIR /app/apps/web -RUN bun run build - -# Production image -FROM base AS runner -WORKDIR /app - -ENV NODE_ENV production -ENV NEXT_TELEMETRY_DISABLED 1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public -COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static - -RUN chown nextjs:nodejs apps - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" - - -CMD ["bun", "apps/web/server.js"] - diff --git a/apps/web/drizzle.config.ts b/apps/web/drizzle.config.ts index a0602a74..2d53d6b9 100644 --- a/apps/web/drizzle.config.ts +++ b/apps/web/drizzle.config.ts @@ -1,19 +1,23 @@ import type { Config } from "drizzle-kit"; import * as dotenv from "dotenv"; +import { webEnv } from "@opencut/env/web"; // Load the right env file based on environment -if (process.env.NODE_ENV === "production") { +if (webEnv.NODE_ENV === "production") { dotenv.config({ path: ".env.production" }); } else { dotenv.config({ path: ".env.local" }); } export default { - schema: "../../packages/db/src/schema.ts", + schema: "./src/schema.ts", dialect: "postgresql", + migrations: { + table: "drizzle_migrations", + }, dbCredentials: { - url: process.env.DATABASE_URL!, + url: webEnv.DATABASE_URL, }, out: "./migrations", - strict: process.env.NODE_ENV === "production", + strict: webEnv.NODE_ENV === "production", } satisfies Config; diff --git a/apps/web/migrations/0000_brainy_saracen.sql b/apps/web/migrations/0000_brainy_saracen.sql new file mode 100644 index 00000000..2c6ab2b2 --- /dev/null +++ b/apps/web/migrations/0000_brainy_saracen.sql @@ -0,0 +1,62 @@ +CREATE TABLE "accounts" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +ALTER TABLE "accounts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "sessions_token_unique" UNIQUE("token") +); +--> statement-breakpoint +ALTER TABLE "sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "users" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean NOT NULL, + "image" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "verifications" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +CREATE TABLE "waitlist" ( + "id" text PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "created_at" timestamp NOT NULL, + CONSTRAINT "waitlist_email_unique" UNIQUE("email") +); +--> statement-breakpoint +ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint +ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/web/migrations/0000_hot_the_fallen.sql b/apps/web/migrations/0000_hot_the_fallen.sql deleted file mode 100644 index 321043f0..00000000 --- a/apps/web/migrations/0000_hot_the_fallen.sql +++ /dev/null @@ -1,54 +0,0 @@ -CREATE TABLE "accounts" ( - "id" text PRIMARY KEY NOT NULL, - "account_id" text NOT NULL, - "provider_id" text NOT NULL, - "user_id" text NOT NULL, - "access_token" text, - "refresh_token" text, - "id_token" text, - "access_token_expires_at" timestamp, - "refresh_token_expires_at" timestamp, - "scope" text, - "password" text, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL -); ---> statement-breakpoint -ALTER TABLE "accounts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -CREATE TABLE "sessions" ( - "id" text PRIMARY KEY NOT NULL, - "expires_at" timestamp NOT NULL, - "token" text NOT NULL, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - "ip_address" text, - "user_agent" text, - "user_id" text NOT NULL, - CONSTRAINT "sessions_token_unique" UNIQUE("token") -); ---> statement-breakpoint -ALTER TABLE "sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -CREATE TABLE "users" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "email" text NOT NULL, - "email_verified" boolean NOT NULL, - "image" text, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - CONSTRAINT "users_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -CREATE TABLE "verifications" ( - "id" text PRIMARY KEY NOT NULL, - "identifier" text NOT NULL, - "value" text NOT NULL, - "expires_at" timestamp NOT NULL, - "created_at" timestamp, - "updated_at" timestamp -); ---> statement-breakpoint -ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/web/migrations/0001_tricky_jackpot.sql b/apps/web/migrations/0001_tricky_jackpot.sql deleted file mode 100644 index 6eaeb56e..00000000 --- a/apps/web/migrations/0001_tricky_jackpot.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE "waitlist" ( - "id" text PRIMARY KEY NOT NULL, - "email" text NOT NULL, - "created_at" timestamp NOT NULL, - CONSTRAINT "waitlist_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY; \ No newline at end of file diff --git a/apps/web/migrations/0002_cuddly_pretty_boy.sql b/apps/web/migrations/0002_cuddly_pretty_boy.sql deleted file mode 100644 index fc8aed79..00000000 --- a/apps/web/migrations/0002_cuddly_pretty_boy.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "users" ALTER COLUMN "email_verified" SET DEFAULT false; \ No newline at end of file diff --git a/apps/web/migrations/0003_long_polaris.sql b/apps/web/migrations/0003_long_polaris.sql deleted file mode 100644 index b2b74064..00000000 --- a/apps/web/migrations/0003_long_polaris.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE "export_waitlist" ( - "id" text PRIMARY KEY NOT NULL, - "email" text NOT NULL, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - CONSTRAINT "export_waitlist_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "export_waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint -DROP TABLE "waitlist" CASCADE; \ No newline at end of file diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json index 2da63d5c..8d505274 100644 --- a/apps/web/migrations/meta/0000_snapshot.json +++ b/apps/web/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e", + "id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -291,6 +291,43 @@ "policies": {}, "checkConstraints": {}, "isRLSEnabled": true + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true } }, "enums": {}, diff --git a/apps/web/migrations/meta/0001_snapshot.json b/apps/web/migrations/meta/0001_snapshot.json deleted file mode 100644 index 008540ad..00000000 --- a/apps/web/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,344 +0,0 @@ -{ - "id": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f", - "prevId": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.accounts": { - "name": "accounts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sessions_token_unique": { - "name": "sessions_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.verifications": { - "name": "verifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.waitlist": { - "name": "waitlist", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "waitlist_email_unique": { - "name": "waitlist_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/apps/web/migrations/meta/0002_snapshot.json b/apps/web/migrations/meta/0002_snapshot.json deleted file mode 100644 index 42512564..00000000 --- a/apps/web/migrations/meta/0002_snapshot.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "id": "1b9148ed-497b-4e53-8cf6-f556c8fe7f7f", - "prevId": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.accounts": { - "name": "accounts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sessions_token_unique": { - "name": "sessions_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.verifications": { - "name": "verifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.waitlist": { - "name": "waitlist", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "waitlist_email_unique": { - "name": "waitlist_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/apps/web/migrations/meta/0003_snapshot.json b/apps/web/migrations/meta/0003_snapshot.json deleted file mode 100644 index 2c5d986e..00000000 --- a/apps/web/migrations/meta/0003_snapshot.json +++ /dev/null @@ -1,365 +0,0 @@ -{ - "id": "71b84015-78f8-4d07-9115-8d56ea550459", - "prevId": "1b9148ed-497b-4e53-8cf6-f556c8fe7f7f", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.accounts": { - "name": "accounts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.export_waitlist": { - "name": "export_waitlist", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "export_waitlist_email_unique": { - "name": "export_waitlist_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "sessions_token_unique": { - "name": "sessions_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.verifications": { - "name": "verifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index c172952e..c7d30f16 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -5,30 +5,9 @@ { "idx": 0, "version": "7", - "when": 1750581188229, - "tag": "0000_hot_the_fallen", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1750689835736, - "tag": "0001_tricky_jackpot", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1752569400809, - "tag": "0002_cuddly_pretty_boy", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1755377469662, - "tag": "0003_long_polaris", + "when": 1750753385927, + "tag": "0000_brainy_saracen", "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts new file mode 100644 index 00000000..c4b7818f --- /dev/null +++ b/apps/web/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/package.json b/apps/web/package.json index 25dc9aee..aa752a81 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,5 +1,5 @@ { - "name": "opencut", + "name": "@opencut/web", "version": "0.1.0", "private": true, "packageManager": "bun@1.2.18", @@ -21,13 +21,18 @@ "@ffmpeg/util": "^0.12.2", "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "^3.9.1", - "@opencut/auth": "workspace:*", - "@opencut/db": "workspace:*", - "@radix-ui/react-separator": "^1.1.7", - "@t3-oss/env-core": "^0.13.8", - "@t3-oss/env-nextjs": "^0.13.8", - "@upstash/ratelimit": "^2.0.5", - "@upstash/redis": "^1.35.0", + "@hugeicons/core-free-icons": "^3.1.1", + "@hugeicons/react": "^1.1.4", + "@huggingface/transformers": "^3.8.1", + "@opencut/env": "workspace:*", + "@opencut/ui": "workspace:*", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tooltip": "^1.2.8", + "@upstash/ratelimit": "^2.0.6", + "@upstash/redis": "^1.35.4", "@vercel/analytics": "^1.4.1", "aws4fetch": "^1.0.20", "better-auth": "^1.2.7", @@ -36,18 +41,19 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "dayjs": "^1.11.13", - "dotenv": "^16.5.0", "drizzle-orm": "^0.44.2", "embla-carousel-react": "^8.5.1", + "eventemitter3": "^5.0.1", "feed": "^5.1.0", - "framer-motion": "^11.13.1", "input-otp": "^1.4.1", - "lucide-react": "^0.468.0", + "lucide-react": "^0.562.0", + "mediabunny": "^1.29.1", "motion": "^12.18.1", "nanoid": "^5.1.5", - "next": "^15.5.7", + "next": "16.1.3", "next-themes": "^0.4.4", "pg": "^8.16.2", + "postgres": "^3.4.5", "radix-ui": "^1.4.2", "react": "^18.2.0", "react-day-picker": "^8.10.1", @@ -67,7 +73,9 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "unified": "^11.0.5", + "use-deep-compare-effect": "^1.8.1", "vaul": "^1.1.1", + "wavesurfer.js": "^7.9.8", "zod": "^3.25.67", "zustand": "^5.0.2" }, @@ -81,6 +89,7 @@ "@types/react-dom": "^18.2.18", "cross-env": "^7.0.3", "drizzle-kit": "^0.31.4", + "dotenv": "^16.5.0", "postcss": "^8", "tailwindcss": "^4.1.11", "tsx": "^4.7.1", diff --git a/apps/web/public/frame.svg b/apps/web/public/frame.svg deleted file mode 100644 index 4dac68e9..00000000 --- a/apps/web/public/frame.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/public/logo.png b/apps/web/public/logo.png deleted file mode 100644 index fc9e7902..00000000 Binary files a/apps/web/public/logo.png and /dev/null differ diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg deleted file mode 100644 index f4079a5e..00000000 --- a/apps/web/public/logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/web/public/logos/opencut/1k/logo-black-with-text.png b/apps/web/public/logos/opencut/1k/logo-black-with-text.png new file mode 100644 index 00000000..08ceb702 Binary files /dev/null and b/apps/web/public/logos/opencut/1k/logo-black-with-text.png differ diff --git a/apps/web/public/logos/opencut/1k/logo-black.png b/apps/web/public/logos/opencut/1k/logo-black.png new file mode 100644 index 00000000..6b21beb9 Binary files /dev/null and b/apps/web/public/logos/opencut/1k/logo-black.png differ diff --git a/apps/web/public/logos/opencut/1k/logo-white-with-text.png b/apps/web/public/logos/opencut/1k/logo-white-with-text.png new file mode 100644 index 00000000..a3151060 Binary files /dev/null and b/apps/web/public/logos/opencut/1k/logo-white-with-text.png differ diff --git a/apps/web/public/logos/opencut/1k/logo-white.png b/apps/web/public/logos/opencut/1k/logo-white.png new file mode 100644 index 00000000..f4f6ae4c Binary files /dev/null and b/apps/web/public/logos/opencut/1k/logo-white.png differ diff --git a/apps/web/public/logos/opencut/2k/logo-black-with-text.png b/apps/web/public/logos/opencut/2k/logo-black-with-text.png new file mode 100644 index 00000000..7493bed6 Binary files /dev/null and b/apps/web/public/logos/opencut/2k/logo-black-with-text.png differ diff --git a/apps/web/public/logos/opencut/2k/logo-black.png b/apps/web/public/logos/opencut/2k/logo-black.png new file mode 100644 index 00000000..1336d172 Binary files /dev/null and b/apps/web/public/logos/opencut/2k/logo-black.png differ diff --git a/apps/web/public/logos/opencut/2k/logo-white-with-text.png b/apps/web/public/logos/opencut/2k/logo-white-with-text.png new file mode 100644 index 00000000..29bf0e6f Binary files /dev/null and b/apps/web/public/logos/opencut/2k/logo-white-with-text.png differ diff --git a/apps/web/public/logos/opencut/2k/logo-white.png b/apps/web/public/logos/opencut/2k/logo-white.png new file mode 100644 index 00000000..9ee83d3c Binary files /dev/null and b/apps/web/public/logos/opencut/2k/logo-white.png differ diff --git a/apps/web/public/logos/opencut/4k/logo-black-with-text.png b/apps/web/public/logos/opencut/4k/logo-black-with-text.png new file mode 100644 index 00000000..edaf5358 Binary files /dev/null and b/apps/web/public/logos/opencut/4k/logo-black-with-text.png differ diff --git a/apps/web/public/logos/opencut/4k/logo-black.png b/apps/web/public/logos/opencut/4k/logo-black.png new file mode 100644 index 00000000..b5f0ff13 Binary files /dev/null and b/apps/web/public/logos/opencut/4k/logo-black.png differ diff --git a/apps/web/public/logos/opencut/4k/logo-white-with-text.png b/apps/web/public/logos/opencut/4k/logo-white-with-text.png new file mode 100644 index 00000000..90596e87 Binary files /dev/null and b/apps/web/public/logos/opencut/4k/logo-white-with-text.png differ diff --git a/apps/web/public/logos/opencut/4k/logo-white.png b/apps/web/public/logos/opencut/4k/logo-white.png new file mode 100644 index 00000000..794e360b Binary files /dev/null and b/apps/web/public/logos/opencut/4k/logo-white.png differ diff --git a/apps/web/public/logos/opencut/svg/logo.svg b/apps/web/public/logos/opencut/svg/logo.svg new file mode 100644 index 00000000..f4079a5e --- /dev/null +++ b/apps/web/public/logos/opencut/svg/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/public/logos/others/fal.svg b/apps/web/public/logos/others/fal.svg new file mode 100644 index 00000000..abc56513 --- /dev/null +++ b/apps/web/public/logos/others/fal.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/logos/others/vercel.svg b/apps/web/public/logos/others/vercel.svg new file mode 100644 index 00000000..824018a3 --- /dev/null +++ b/apps/web/public/logos/others/vercel.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx deleted file mode 100644 index f28cd191..00000000 --- a/apps/web/src/app/(auth)/login/page.tsx +++ /dev/null @@ -1,146 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { memo, Suspense } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import Link from "next/link"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { ArrowLeft, Loader2 } from "lucide-react"; -import { GoogleIcon } from "@/components/icons"; -import { useLogin } from "@/hooks/auth/useLogin"; - -const LoginPage = () => { - const router = useRouter(); - const { - email, - setEmail, - password, - setPassword, - error, - isAnyLoading, - isEmailLoading, - isGoogleLoading, - handleLogin, - handleGoogleLogin, - } = useLogin(); - - return ( -
- - - - Welcome back - - Sign in to your account to continue - - - - - -
- } - > -
- {error && ( - - Error - {error} - - )} - - -
-
- -
-
- - Or continue with - -
-
-
-
- - setEmail(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setPassword(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
- -
-
-
- Don't have an account?{" "} - - Sign up - -
- - - - - ); -}; - -export default memo(LoginPage); diff --git a/apps/web/src/app/(auth)/signup/page.tsx b/apps/web/src/app/(auth)/signup/page.tsx deleted file mode 100644 index c3a0f899..00000000 --- a/apps/web/src/app/(auth)/signup/page.tsx +++ /dev/null @@ -1,162 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { memo, Suspense } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import Link from "next/link"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { ArrowLeft, Loader2 } from "lucide-react"; -import { GoogleIcon } from "@/components/icons"; -import { useSignUp } from "@/hooks/auth/useSignUp"; - -const SignUpPage = () => { - const router = useRouter(); - const { - name, - setName, - email, - setEmail, - password, - setPassword, - error, - isAnyLoading, - isEmailLoading, - isGoogleLoading, - handleSignUp, - handleGoogleSignUp, - } = useSignUp(); - - return ( -
- - - - - Create your account - - - Get started with your free account today - - - - - -
- } - > -
- {error && ( - - Error - {error} - - )} - -
-
- -
-
- - Or continue with - -
-
-
-
- - setName(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setEmail(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
-
- - setPassword(e.target.value)} - disabled={isAnyLoading} - className="h-11" - /> -
- -
-
-
- Already have an account?{" "} - - Sign in - -
- - - - - ); -}; - -export default memo(SignUpPage); diff --git a/apps/web/src/app/animation/page.tsx b/apps/web/src/app/animation/page.tsx deleted file mode 100644 index 37be1ef0..00000000 --- a/apps/web/src/app/animation/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { InputWithBack } from "@/components/ui/input-with-back"; -import { useState } from "react"; - -export default function AnimationPage() { - const [isExpanded, setIsExpanded] = useState(false); - - return ( -
-
- -
-
-

setIsExpanded(!isExpanded)} - className="cursor-pointer hover:opacity-75 transition-opacity" - > - {isExpanded ? "Collapse" : "Expand"} -

-
-
- ); -} diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts index 5deef204..e17d0cce 100644 --- a/apps/web/src/app/api/auth/[...all]/route.ts +++ b/apps/web/src/app/api/auth/[...all]/route.ts @@ -1,4 +1,4 @@ -import { auth } from "@opencut/auth"; +import { auth } from "@/lib/auth/server"; import { toNextJsHandler } from "better-auth/next-js"; export const { POST, GET } = toNextJsHandler(auth); diff --git a/apps/web/src/app/api/get-upload-url/route.ts b/apps/web/src/app/api/get-upload-url/route.ts deleted file mode 100644 index dc5b7328..00000000 --- a/apps/web/src/app/api/get-upload-url/route.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { AwsClient } from "aws4fetch"; -import { nanoid } from "nanoid"; -import { env } from "@/env"; -import { baseRateLimit } from "@/lib/rate-limit"; -import { isTranscriptionConfigured } from "@/lib/transcription-utils"; - -const uploadRequestSchema = z.object({ - fileExtension: z.enum(["wav", "mp3", "m4a", "flac"], { - errorMap: () => ({ - message: "File extension must be wav, mp3, m4a, or flac", - }), - }), -}); - -const apiResponseSchema = z.object({ - uploadUrl: z.string().url(), - fileName: z.string().min(1), -}); - -export async function POST(request: NextRequest) { - try { - // Rate limiting - const ip = request.headers.get("x-forwarded-for") ?? "anonymous"; - const { success } = await baseRateLimit.limit(ip); - - if (!success) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } - - // Check transcription configuration - const transcriptionCheck = isTranscriptionConfigured(); - if (!transcriptionCheck.configured) { - console.error( - "Missing environment variables:", - JSON.stringify(transcriptionCheck.missingVars) - ); - - return NextResponse.json( - { - error: "Transcription not configured", - message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`, - }, - { status: 503 } - ); - } - - // Parse and validate request body - const rawBody = await request.json().catch(() => null); - if (!rawBody) { - return NextResponse.json( - { error: "Invalid JSON in request body" }, - { status: 400 } - ); - } - - const validationResult = uploadRequestSchema.safeParse(rawBody); - if (!validationResult.success) { - return NextResponse.json( - { - error: "Invalid request parameters", - details: validationResult.error.flatten().fieldErrors, - }, - { status: 400 } - ); - } - - const { fileExtension } = validationResult.data; - - // Initialize R2 client - const client = new AwsClient({ - accessKeyId: env.R2_ACCESS_KEY_ID, - secretAccessKey: env.R2_SECRET_ACCESS_KEY, - }); - - // Generate unique filename with timestamp - const timestamp = Date.now(); - const fileName = `audio/${timestamp}-${nanoid()}.${fileExtension}`; - - // Create presigned URL - const url = new URL( - `https://${env.R2_BUCKET_NAME}.${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}` - ); - - url.searchParams.set("X-Amz-Expires", "3600"); // 1 hour expiry - - const signed = await client.sign(new Request(url, { method: "PUT" }), { - aws: { signQuery: true }, - }); - - if (!signed.url) { - throw new Error("Failed to generate presigned URL"); - } - - // Prepare and validate response - const responseData = { - uploadUrl: signed.url, - fileName, - }; - - const responseValidation = apiResponseSchema.safeParse(responseData); - if (!responseValidation.success) { - console.error( - "Invalid API response structure:", - responseValidation.error - ); - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 } - ); - } - - return NextResponse.json(responseValidation.data); - } catch (error) { - console.error("Error generating upload URL:", error); - return NextResponse.json( - { - error: "Failed to generate upload URL", - message: - error instanceof Error - ? error.message - : "An unexpected error occurred", - }, - { status: 500 } - ); - } -} diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts index ecb9ce04..edb40a0d 100644 --- a/apps/web/src/app/api/health/route.ts +++ b/apps/web/src/app/api/health/route.ts @@ -1,5 +1,3 @@ -import { NextRequest } from "next/server"; - -export async function GET(request: NextRequest) { - return new Response("OK", { status: 200 }); +export async function GET() { + return new Response("OK", { status: 200 }); } diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts index c89bc76c..3070d152 100644 --- a/apps/web/src/app/api/sounds/search/route.ts +++ b/apps/web/src/app/api/sounds/search/route.ts @@ -1,265 +1,280 @@ -import { NextRequest, NextResponse } from "next/server"; +import { webEnv } from "@opencut/env/web"; +import { type NextRequest, NextResponse } from "next/server"; import { z } from "zod"; -import { env } from "@/env"; -import { baseRateLimit } from "@/lib/rate-limit"; +import { checkRateLimit } from "@/lib/rate-limit"; const searchParamsSchema = z.object({ - q: z.string().max(500, "Query too long").optional(), - type: z.enum(["songs", "effects"]).optional(), - page: z.coerce.number().int().min(1).max(1000).default(1), - page_size: z.coerce.number().int().min(1).max(150).default(20), - sort: z - .enum(["downloads", "rating", "created", "score"]) - .default("downloads"), - min_rating: z.coerce.number().min(0).max(5).default(3), - commercial_only: z.coerce.boolean().default(true), + q: z.string().max(500, "Query too long").optional(), + type: z.enum(["songs", "effects"]).optional(), + page: z.coerce.number().int().min(1).max(1000).default(1), + page_size: z.coerce.number().int().min(1).max(150).default(20), + sort: z + .enum(["downloads", "rating", "created", "score"]) + .default("downloads"), + min_rating: z.coerce.number().min(0).max(5).default(3), + commercial_only: z.coerce.boolean().default(true), }); const freesoundResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string().url(), - previews: z - .object({ - "preview-hq-mp3": z.string().url(), - "preview-lq-mp3": z.string().url(), - "preview-hq-ogg": z.string().url(), - "preview-lq-ogg": z.string().url(), - }) - .optional(), - download: z.string().url().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - num_downloads: z.number().optional(), - avg_rating: z.number().optional(), - num_ratings: z.number().optional(), + id: z.number(), + name: z.string(), + description: z.string(), + url: z.string().url(), + previews: z + .object({ + "preview-hq-mp3": z.string().url(), + "preview-lq-mp3": z.string().url(), + "preview-hq-ogg": z.string().url(), + "preview-lq-ogg": z.string().url(), + }) + .optional(), + download: z.string().url().optional(), + duration: z.number(), + filesize: z.number(), + type: z.string(), + channels: z.number(), + bitrate: z.number(), + bitdepth: z.number(), + samplerate: z.number(), + username: z.string(), + tags: z.array(z.string()), + license: z.string(), + created: z.string(), + num_downloads: z.number().optional(), + avg_rating: z.number().optional(), + num_ratings: z.number().optional(), }); const freesoundResponseSchema = z.object({ - count: z.number(), - next: z.string().url().nullable(), - previous: z.string().url().nullable(), - results: z.array(freesoundResultSchema), + count: z.number(), + next: z.string().url().nullable(), + previous: z.string().url().nullable(), + results: z.array(freesoundResultSchema), }); const transformedResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string(), - previewUrl: z.string().optional(), - downloadUrl: z.string().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - downloads: z.number().optional(), - rating: z.number().optional(), - ratingCount: z.number().optional(), + id: z.number(), + name: z.string(), + description: z.string(), + url: z.string(), + previewUrl: z.string().optional(), + downloadUrl: z.string().optional(), + duration: z.number(), + filesize: z.number(), + type: z.string(), + channels: z.number(), + bitrate: z.number(), + bitdepth: z.number(), + samplerate: z.number(), + username: z.string(), + tags: z.array(z.string()), + license: z.string(), + created: z.string(), + downloads: z.number().optional(), + rating: z.number().optional(), + ratingCount: z.number().optional(), }); const apiResponseSchema = z.object({ - count: z.number(), - next: z.string().nullable(), - previous: z.string().nullable(), - results: z.array(transformedResultSchema), - query: z.string().optional(), - type: z.string(), - page: z.number(), - pageSize: z.number(), - sort: z.string(), - minRating: z.number().optional(), + count: z.number(), + next: z.string().nullable(), + previous: z.string().nullable(), + results: z.array(transformedResultSchema), + query: z.string().optional(), + type: z.string(), + page: z.number(), + pageSize: z.number(), + sort: z.string(), + minRating: z.number().optional(), }); -export async function GET(request: NextRequest) { - try { - const ip = request.headers.get("x-forwarded-for") ?? "anonymous"; - const { success } = await baseRateLimit.limit(ip); +function buildSortParameter({ query, sort }: { query?: string; sort: string }) { + if (!query) return `${sort}_desc`; + return sort === "score" ? "score" : `${sort}_desc`; +} - if (!success) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } +function applyEffectsFilters({ + params, + min_rating, + commercial_only, +}: { + params: URLSearchParams; + min_rating: number; + commercial_only: boolean; +}) { + params.append("filter", "duration:[* TO 30.0]"); + params.append("filter", `avg_rating:[${min_rating} TO *]`); - const { searchParams } = new URL(request.url); + if (commercial_only) { + params.append( + "filter", + 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")', + ); + } + + params.append( + "filter", + "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion", + ); +} - const validationResult = searchParamsSchema.safeParse({ - q: searchParams.get("q") || undefined, - type: searchParams.get("type") || undefined, - page: searchParams.get("page") || undefined, - page_size: searchParams.get("page_size") || undefined, - sort: searchParams.get("sort") || undefined, - min_rating: searchParams.get("min_rating") || undefined, - }); +function transformFreesoundResult( + result: z.infer, +) { + return { + id: result.id, + name: result.name, + description: result.description, + url: result.url, + previewUrl: + result.previews?.["preview-hq-mp3"] || + result.previews?.["preview-lq-mp3"], + downloadUrl: result.download, + duration: result.duration, + filesize: result.filesize, + type: result.type, + channels: result.channels, + bitrate: result.bitrate, + bitdepth: result.bitdepth, + samplerate: result.samplerate, + username: result.username, + tags: result.tags, + license: result.license, + created: result.created, + downloads: result.num_downloads || 0, + rating: result.avg_rating || 0, + ratingCount: result.num_ratings || 0, + }; +} + +export async function GET(request: NextRequest) { + try { + const { limited } = await checkRateLimit({ request }); + if (limited) { + return NextResponse.json({ error: "Too many requests" }, { status: 429 }); + } - if (!validationResult.success) { - return NextResponse.json( - { - error: "Invalid parameters", - details: validationResult.error.flatten().fieldErrors, - }, - { status: 400 } - ); - } + const { searchParams } = new URL(request.url); - const { - q: query, - type, - page, - page_size: pageSize, - sort, - min_rating, - commercial_only, - } = validationResult.data; + const validationResult = searchParamsSchema.safeParse({ + q: searchParams.get("q") || undefined, + type: searchParams.get("type") || undefined, + page: searchParams.get("page") || undefined, + page_size: searchParams.get("page_size") || undefined, + sort: searchParams.get("sort") || undefined, + min_rating: searchParams.get("min_rating") || undefined, + }); - if (type === "songs") { - return NextResponse.json( - { - error: "Songs are not available yet", - message: - "Song search functionality is coming soon. Try searching for sound effects instead.", - }, - { status: 501 } - ); - } + if (!validationResult.success) { + return NextResponse.json( + { + error: "Invalid parameters", + details: validationResult.error.flatten().fieldErrors, + }, + { status: 400 }, + ); + } - const baseUrl = "https://freesound.org/apiv2/search/text/"; + const { + q: query, + type, + page, + page_size: pageSize, + sort, + min_rating, + commercial_only, + } = validationResult.data; - // Use score sorting for search queries, downloads for top sounds - const sortParam = query - ? sort === "score" - ? "score" - : `${sort}_desc` - : `${sort}_desc`; + if (type === "songs") { + return NextResponse.json( + { + error: "Songs are not available yet", + message: + "Song search functionality is coming soon. Try searching for sound effects instead.", + }, + { status: 501 }, + ); + } - const params = new URLSearchParams({ - query: query || "", - token: env.FREESOUND_API_KEY, - page: page.toString(), - page_size: pageSize.toString(), - sort: sortParam, - fields: - "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", - }); + const baseUrl = "https://freesound.org/apiv2/search/text/"; - // Always apply sound effect filters (since we're primarily a sound effects search) - if (type === "effects" || !type) { - params.append("filter", "duration:[* TO 30.0]"); - params.append("filter", `avg_rating:[${min_rating} TO *]`); + const sortParam = buildSortParameter({ query, sort }); - // Filter by license if commercial_only is true - if (commercial_only) { - params.append( - "filter", - 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")' - ); - } + const params = new URLSearchParams({ + query: query || "", + token: webEnv.FREESOUND_API_KEY, + page: page.toString(), + page_size: pageSize.toString(), + sort: sortParam, + fields: + "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", + }); - params.append( - "filter", - "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion" - ); - } + const isEffectsSearch = type === "effects" || !type; + if (isEffectsSearch) { + applyEffectsFilters({ params, min_rating, commercial_only }); + } - const response = await fetch(`${baseUrl}?${params.toString()}`); + const response = await fetch(`${baseUrl}?${params.toString()}`); - if (!response.ok) { - const errorText = await response.text(); - console.error("Freesound API error:", response.status, errorText); - return NextResponse.json( - { error: "Failed to search sounds" }, - { status: response.status } - ); - } + if (!response.ok) { + const errorText = await response.text(); + console.error("Freesound API error:", response.status, errorText); + return NextResponse.json( + { error: "Failed to search sounds" }, + { status: response.status }, + ); + } - const rawData = await response.json(); + const rawData = await response.json(); - const freesoundValidation = freesoundResponseSchema.safeParse(rawData); - if (!freesoundValidation.success) { - console.error( - "Invalid Freesound API response:", - freesoundValidation.error - ); - return NextResponse.json( - { error: "Invalid response from Freesound API" }, - { status: 502 } - ); - } + const freesoundValidation = freesoundResponseSchema.safeParse(rawData); + if (!freesoundValidation.success) { + console.error( + "Invalid Freesound API response:", + freesoundValidation.error, + ); + return NextResponse.json( + { error: "Invalid response from Freesound API" }, + { status: 502 }, + ); + } - const data = freesoundValidation.data; + const data = freesoundValidation.data; - const transformedResults = data.results.map((result) => ({ - id: result.id, - name: result.name, - description: result.description, - url: result.url, - previewUrl: - result.previews?.["preview-hq-mp3"] || - result.previews?.["preview-lq-mp3"], - downloadUrl: result.download, - duration: result.duration, - filesize: result.filesize, - type: result.type, - channels: result.channels, - bitrate: result.bitrate, - bitdepth: result.bitdepth, - samplerate: result.samplerate, - username: result.username, - tags: result.tags, - license: result.license, - created: result.created, - downloads: result.num_downloads || 0, - rating: result.avg_rating || 0, - ratingCount: result.num_ratings || 0, - })); + const transformedResults = data.results.map(transformFreesoundResult); - const responseData = { - count: data.count, - next: data.next, - previous: data.previous, - results: transformedResults, - query: query || "", - type: type || "effects", - page, - pageSize, - sort, - minRating: min_rating, - }; + const responseData = { + count: data.count, + next: data.next, + previous: data.previous, + results: transformedResults, + query: query || "", + type: type || "effects", + page, + pageSize, + sort, + minRating: min_rating, + }; - const responseValidation = apiResponseSchema.safeParse(responseData); - if (!responseValidation.success) { - console.error( - "Invalid API response structure:", - responseValidation.error - ); - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 } - ); - } + const responseValidation = apiResponseSchema.safeParse(responseData); + if (!responseValidation.success) { + console.error( + "Invalid API response structure:", + responseValidation.error, + ); + return NextResponse.json( + { error: "Internal response formatting error" }, + { status: 500 }, + ); + } - return NextResponse.json(responseValidation.data); - } catch (error) { - console.error("Error searching sounds:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } + return NextResponse.json(responseValidation.data); + } catch (error) { + console.error("Error searching sounds:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } } diff --git a/apps/web/src/app/api/transcribe/route.ts b/apps/web/src/app/api/transcribe/route.ts deleted file mode 100644 index 9a497f65..00000000 --- a/apps/web/src/app/api/transcribe/route.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { env } from "@/env"; -import { baseRateLimit } from "@/lib/rate-limit"; -import { isTranscriptionConfigured } from "@/lib/transcription-utils"; - -const transcribeRequestSchema = z.object({ - filename: z.string().min(1, "Filename is required"), - language: z.string().optional().default("auto"), - decryptionKey: z.string().min(1, "Decryption key is required").optional(), - iv: z.string().min(1, "IV is required").optional(), -}); - -const modalResponseSchema = z.object({ - text: z.string(), - segments: z.array( - z.object({ - id: z.number(), - seek: z.number(), - start: z.number(), - end: z.number(), - text: z.string(), - tokens: z.array(z.number()), - temperature: z.number(), - avg_logprob: z.number(), - compression_ratio: z.number(), - no_speech_prob: z.number(), - }) - ), - language: z.string(), -}); - -const apiResponseSchema = z.object({ - text: z.string(), - segments: z.array( - z.object({ - id: z.number(), - seek: z.number(), - start: z.number(), - end: z.number(), - text: z.string(), - tokens: z.array(z.number()), - temperature: z.number(), - avg_logprob: z.number(), - compression_ratio: z.number(), - no_speech_prob: z.number(), - }) - ), - language: z.string(), -}); - -export async function POST(request: NextRequest) { - try { - // Rate limiting - const ip = request.headers.get("x-forwarded-for") ?? "anonymous"; - const { success } = await baseRateLimit.limit(ip); - const origin = request.headers.get("origin"); - - if (!success) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } - - // Check transcription configuration - const transcriptionCheck = isTranscriptionConfigured(); - if (!transcriptionCheck.configured) { - console.error( - "Missing environment variables:", - JSON.stringify(transcriptionCheck.missingVars) - ); - - return NextResponse.json( - { - error: "Transcription not configured", - message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`, - }, - { status: 503 } - ); - } - - // Parse and validate request body - const rawBody = await request.json().catch(() => null); - if (!rawBody) { - return NextResponse.json( - { error: "Invalid JSON in request body" }, - { status: 400 } - ); - } - - const validationResult = transcribeRequestSchema.safeParse(rawBody); - if (!validationResult.success) { - return NextResponse.json( - { - error: "Invalid request parameters", - details: validationResult.error.flatten().fieldErrors, - }, - { status: 400 } - ); - } - - const { filename, language, decryptionKey, iv } = validationResult.data; - - // Prepare request body for Modal - const modalRequestBody: any = { - filename, - language, - }; - - // Add encryption parameters if provided (zero-knowledge) - if (decryptionKey && iv) { - modalRequestBody.decryptionKey = decryptionKey; - modalRequestBody.iv = iv; - } - - // Call Modal transcription service - const response = await fetch(env.MODAL_TRANSCRIPTION_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(modalRequestBody), - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error("Modal API error:", response.status, errorText); - - let errorMessage = "Transcription service unavailable"; - try { - const errorData = JSON.parse(errorText); - errorMessage = errorData.error || errorMessage; - } catch { - // Use default message if parsing fails - } - - return NextResponse.json( - { - error: errorMessage, - message: "Failed to process transcription request", - }, - { status: response.status >= 500 ? 502 : response.status } - ); - } - - const rawResult = await response.json(); - console.log("Raw Modal response:", JSON.stringify(rawResult, null, 2)); - - // Validate Modal response - const modalValidation = modalResponseSchema.safeParse(rawResult); - if (!modalValidation.success) { - console.error("Invalid Modal API response:", modalValidation.error); - return NextResponse.json( - { error: "Invalid response from transcription service" }, - { status: 502 } - ); - } - - const result = modalValidation.data; - - // Prepare and validate API response - const responseData = { - text: result.text, - segments: result.segments, - language: result.language, - }; - - const responseValidation = apiResponseSchema.safeParse(responseData); - if (!responseValidation.success) { - console.error( - "Invalid API response structure:", - responseValidation.error - ); - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 } - ); - } - - return NextResponse.json(responseValidation.data); - } catch (error) { - console.error("Transcription API error:", error); - return NextResponse.json( - { - error: "Internal server error", - message: "An unexpected error occurred during transcription", - }, - { status: 500 } - ); - } -} diff --git a/apps/web/src/app/api/waitlist/export/route.ts b/apps/web/src/app/api/waitlist/export/route.ts deleted file mode 100644 index 0200e255..00000000 --- a/apps/web/src/app/api/waitlist/export/route.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { baseRateLimit } from "@/lib/rate-limit"; -import { db, exportWaitlist, eq } from "@opencut/db"; -import { randomUUID } from "crypto"; -import { - exportWaitlistSchema, - exportWaitlistResponseSchema, -} from "@/lib/schemas/waitlist"; - -const requestSchema = exportWaitlistSchema; -const responseSchema = exportWaitlistResponseSchema; - -export async function POST(request: NextRequest) { - try { - const ip = request.headers.get("x-forwarded-for") ?? "anonymous"; - const { success } = await baseRateLimit.limit(ip); - if (!success) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } - - const body = await request.json().catch(() => null); - if (!body) { - return NextResponse.json( - { error: "Invalid JSON in request body" }, - { status: 400 } - ); - } - - const parsed = requestSchema.safeParse(body); - if (!parsed.success) { - return NextResponse.json( - { - error: "Invalid request parameters", - details: parsed.error.flatten().fieldErrors, - }, - { status: 400 } - ); - } - - const { email } = parsed.data; - - const existing = await db - .select({ id: exportWaitlist.id }) - .from(exportWaitlist) - .where(eq(exportWaitlist.email, email)) - .limit(1); - - if (existing.length > 0) { - const responseData = { success: true, alreadySubscribed: true } as const; - const validated = responseSchema.safeParse(responseData); - if (!validated.success) { - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 } - ); - } - return NextResponse.json(validated.data); - } - - await db.insert(exportWaitlist).values({ - id: randomUUID(), - email, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const responseData = { success: true } as const; - const validated = responseSchema.safeParse(responseData); - if (!validated.success) { - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 } - ); - } - return NextResponse.json(validated.data); - } catch (error) { - console.error("Waitlist API error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -} diff --git a/apps/web/src/app/base-page.tsx b/apps/web/src/app/base-page.tsx new file mode 100644 index 00000000..a3d56fe5 --- /dev/null +++ b/apps/web/src/app/base-page.tsx @@ -0,0 +1,53 @@ +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { cn } from "@/utils/ui"; + +interface BasePageProps { + children: React.ReactNode; + className?: string; + mainClassName?: string; + maxWidth?: "3xl" | "6xl" | "full"; + title?: string; + description?: string; +} + +export function BasePage({ + children, + className = "", + mainClassName = "", + maxWidth = "3xl", + title, + description, +}: BasePageProps) { + const maxWidthClass = { + "3xl": "max-w-3xl", + "6xl": "max-w-6xl", + full: "max-w-full", + }[maxWidth]; + + return ( +
+
+
+ {title && description && ( +
+

+ {title} +

+

+ {description} +

+
+ )} + {children} +
+
+
+ ); +} diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx index d759050b..46c6163d 100644 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -1,144 +1,152 @@ -import { Header } from "@/components/header"; -import Prose from "@/components/ui/prose"; -import { Separator } from "@/components/ui/separator"; -import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog-query"; -import { Metadata } from "next"; +import type { Metadata } from "next"; import Image from "next/image"; import { notFound } from "next/navigation"; +import { BasePage } from "@/app/base-page"; +import Prose from "@/components/ui/prose"; +import { Separator } from "@/components/ui/separator"; +import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query"; +import type { Author, Post } from "@/types/blog"; type PageProps = { - params: Promise<{ slug: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + params: Promise<{ slug: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }; export async function generateMetadata({ - params, + params, }: PageProps): Promise { - const slug = (await params).slug; - - const data = await getSinglePost(slug); - - if (!data || !data.post) return {}; - - return { - title: data.post.title, - description: data.post.description, - twitter: { - title: `${data.post.title}`, - description: `${data.post.description}`, - card: "summary_large_image", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - }, - openGraph: { - type: "article", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - title: data.post.title, - description: data.post.description, - publishedTime: new Date(data.post.publishedAt).toISOString(), - authors: [ - ...data.post.authors.map((author: { name: string }) => author.name), - ], - }, - }; + const slug = (await params).slug; + + const data = await getSinglePost({ slug }); + + if (!data || !data.post) return {}; + + return { + title: data.post.title, + description: data.post.description, + twitter: { + title: `${data.post.title}`, + description: `${data.post.description}`, + card: "summary_large_image", + images: [ + { + url: data.post.coverImage, + width: "1200", + height: "630", + alt: data.post.title, + }, + ], + }, + openGraph: { + type: "article", + images: [ + { + url: data.post.coverImage, + width: "1200", + height: "630", + alt: data.post.title, + }, + ], + title: data.post.title, + description: data.post.description, + publishedTime: new Date(data.post.publishedAt).toISOString(), + authors: data.post.authors.map((author: Author) => author.name), + }, + }; } export async function generateStaticParams() { - const data = await getPosts(); - if (!data || !data.posts.length) return []; + const data = await getPosts(); + if (!data || !data.posts.length) return []; + + return data.posts.map((post) => ({ + slug: post.slug, + })); +} + +export default async function BlogPostPage({ params }: PageProps) { + const slug = (await params).slug; + const data = await getSinglePost({ slug }); + if (!data || !data.post) return notFound(); + + const html = await processHtmlContent({ html: data.post.content }); - return data.posts.map((post) => ({ - slug: post.slug, - })); + return ( + + + + + + ); } -async function Page({ params }: PageProps) { - const slug = (await params).slug; - const data = await getSinglePost(slug); - if (!data || !data.post) return notFound(); - - const html = await processHtmlContent(data.post.content); - - const formattedDate = new Date(data.post.publishedAt).toLocaleDateString( - "en-US", - { - day: "numeric", - month: "long", - year: "numeric", - } - ); - - return ( -
-
- -
-
-
-
-
- -
-
- {data.post.coverImage && ( -
- {data.post.title} -
- )} -
- -
- -

- {data.post.title} -

-
- {data.post.authors[0] && ( - <> - {data.post.authors[0].name} -

- {data.post.authors[0].name} -

- - )} -
-
- -
- -
-
-
-
- ); +function PostHeader({ post }: { post: Post }) { + const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", { + day: "numeric", + month: "long", + year: "numeric", + }); + + return ( + <> + {post.coverImage && } + + + + + ); +} + +function PostCoverImage({ post }: { post: Post }) { + return ( +
+ {post.title} +
+ ); } -export default Page; +function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) { + return ( +
+ +
+ ); +} + +function PostTitle({ title }: { title: string }) { + return ( +

{title}

+ ); +} + +function PostAuthor({ author }: { author?: Author }) { + if (!author) return null; + + return ( +
+ {author.name} +

{author.name}

+
+ ); +} + +function PostContent({ html }: { html: string }) { + return ( +
+ +
+ ); +} diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx index 08da9fd7..9e3b6d11 100644 --- a/apps/web/src/app/blog/page.tsx +++ b/apps/web/src/app/blog/page.tsx @@ -1,98 +1,73 @@ -import { Metadata } from "next"; -import { Header } from "@/components/header"; -import { Card, CardContent } from "@/components/ui/card"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import type { Metadata } from "next"; import Link from "next/link"; -import { getPosts } from "@/lib/blog-query"; -import Image from "next/image"; +import { BasePage } from "@/app/base-page"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Separator } from "@/components/ui/separator"; +import { getPosts } from "@/lib/blog/query"; +import type { Author, Post } from "@/types/blog"; export const metadata: Metadata = { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - openGraph: { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - type: "website", - }, + title: "Blog - OpenCut", + description: + "Read the latest news and updates about OpenCut, the free and open-source video editor.", + openGraph: { + title: "Blog - OpenCut", + description: + "Read the latest news and updates about OpenCut, the free and open-source video editor.", + type: "website", + }, }; export default async function BlogPage() { - const data = await getPosts(); - if (!data || !data.posts) return
No posts yet
; - - return ( -
-
+ const data = await getPosts(); + if (!data || !data.posts) return
No posts yet
; -
-
-
-
-
+ return ( + +
+ {data.posts.map((post) => ( +
+ + +
+ ))} +
+
+ ); +} -
-
-

- Blog -

-

- Read the latest news and updates about OpenCut, the free and - open-source video editor. -

-
-
- {data.posts.map((post) => ( - - - {post.coverImage && ( -
- {post.title} -
- )} +function BlogPostItem({ post }: { post: Post }) { + return ( + +
+
+

{post.title}

+

{post.description}

+
+ {post.authors && post.authors.length > 0 && ( + + )} +
+ + ); +} - - {post.authors && post.authors.length > 0 && ( -
- {post.authors.map((author, index) => ( -
- - - - {author.name.charAt(0).toUpperCase()} - - - - {author.name} - - {index < post.authors.length - 1 && ( - • - )} -
- ))} -
- )} -

{post.title}

-

{post.description}

-
-
- - ))} -
-
-
-
- ); +function AuthorList({ authors }: { authors: Author[] }) { + return ( +
+ {authors.map((author) => ( +
+ + + + {author.name.charAt(0).toUpperCase()} + + +
+ ))} +
+ ); } diff --git a/apps/web/src/app/contributors/page.tsx b/apps/web/src/app/contributors/page.tsx index 3ad99023..21703852 100644 --- a/apps/web/src/app/contributors/page.tsx +++ b/apps/web/src/app/contributors/page.tsx @@ -1,330 +1,242 @@ -import { Metadata } from "next"; -import { Header } from "@/components/header"; -import { Card, CardContent } from "@/components/ui/card"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { ExternalLink } from "lucide-react"; +import type { Metadata } from "next"; import Link from "next/link"; -import { - GithubIcon, - MarbleIcon, - VercelIcon, - DataBuddyIcon, -} from "@/components/icons"; -import { Badge } from "@/components/ui/badge"; -import { EXTERNAL_TOOLS } from "@/constants/site"; +import { GitHubContributeSection } from "@/components/gitHub-contribute-section"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Card, CardContent } from "@/components/ui/card"; +import { EXTERNAL_TOOLS } from "@/constants/site-constants"; +import { BasePage } from "../base-page"; export const metadata: Metadata = { - title: "Contributors - OpenCut", - description: - "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", - openGraph: { - title: "Contributors - OpenCut", - description: - "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", - type: "website", - }, + title: "Contributors - OpenCut", + description: + "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", + openGraph: { + title: "Contributors - OpenCut", + description: + "Meet the amazing people who contribute to OpenCut, the free and open-source video editor.", + type: "website", + }, }; interface Contributor { - id: number; - login: string; - avatar_url: string; - html_url: string; - contributions: number; - type: string; + id: number; + login: string; + avatar_url: string; + html_url: string; + contributions: number; + type: string; } async function getContributors(): Promise { - try { - const response = await fetch( - "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100", - { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "OpenCut-Web-App", - }, - next: { revalidate: 600 }, // 10 minutes - } as RequestInit - ); - - if (!response.ok) { - console.error("Failed to fetch contributors"); - return []; - } - - const contributors = (await response.json()) as Contributor[]; - - const filteredContributors = contributors.filter( - (contributor: Contributor) => contributor.type === "User" - ); - - return filteredContributors; - } catch (error) { - console.error("Error fetching contributors:", error); - return []; - } + try { + const response = await fetch( + "https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100", + { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "OpenCut-Web-App", + }, + next: { revalidate: 600 }, // 10 minutes + }, + ); + + if (!response.ok) { + console.error("Failed to fetch contributors"); + return []; + } + + const contributors = (await response.json()) as Contributor[]; + + const filteredContributors = contributors.filter( + (contributor) => contributor.type === "User", + ); + + return filteredContributors; + } catch (error) { + console.error("Error fetching contributors:", error); + return []; + } } export default async function ContributorsPage() { - const contributors = await getContributors(); - const topContributors = contributors.slice(0, 2); - const otherContributors = contributors.slice(2); - - return ( -
-
- -
-
-
-
-
- -
-
-
- - - - Open Source - - -

- Contributors -

-

- Meet the amazing developers who are building the future of video - editing -

- -
-
-
- {contributors.length} - contributors -
-
-
- - {contributors.reduce((sum, c) => sum + c.contributions, 0)} - - contributions -
-
-
- - {topContributors.length > 0 && ( -
-
-

- Top Contributors -

-

- Leading the way in contributions -

-
- -
- {topContributors.map((contributor, index) => ( - -
-
- - -
- - - - {contributor.login.charAt(0).toUpperCase()} - - -
-

- {contributor.login} -

-
- - {contributor.contributions} - - contributions -
-
-
-
- - ))} -
-
- )} - - {otherContributors.length > 0 && ( -
-
-

- All Contributors -

-

- Everyone who makes OpenCut better -

-
- -
- {otherContributors.map((contributor, index) => ( - -
- - - - {contributor.login.charAt(0).toUpperCase()} - - -

- {contributor.login} -

-

- {contributor.contributions} -

-
- - ))} -
-
- )} - - {contributors.length === 0 && ( -
-
- -
-

- No contributors found -

-

- Unable to load contributors at the moment. Check back later or - view on GitHub. -

- - - -
- )} + const contributors = await getContributors(); + const topContributors = contributors.slice(0, 2); + const otherContributors = contributors.slice(2); + const totalContributions = contributors.reduce( + (sum, c) => sum + c.contributions, + 0, + ); + + return ( + +
+ + +
+ +
+ {topContributors.length > 0 && ( + + )} + {otherContributors.length > 0 && ( + + )} + + +
+
+ ); +} -
-
-

External Tools

-

- Tools we use to build OpenCut -

-
+function StatItem({ value, label }: { value: number; label: string }) { + return ( +
+
+ {value} + {label} +
+ ); +} -
- {EXTERNAL_TOOLS.map((tool, index) => { - const IconComponent = { - MarbleIcon, - VercelIcon, - DataBuddyIcon, - }[tool.icon]; +function TopContributorsSection({ + contributors, +}: { + contributors: Contributor[]; +}) { + return ( +
+
+

Top contributors

+

+ Leading the way in contributions +

+
+ +
+ {contributors.map((contributor) => ( + + ))} +
+
+ ); +} - return ( - - - -
-
- -
-
-

- {tool.name} -

-

- {tool.description} -

-
-
- - ); - })} -
-
+function TopContributorCard({ contributor }: { contributor: Contributor }) { + return ( + + + + + + + {contributor.login.charAt(0).toUpperCase()} + + +
+

{contributor.login}

+
+ {contributor.contributions} + contributions +
+
+
+
+ + ); +} -
-
-

Join the community

-

- OpenCut is built by developers like you. Every contribution, - no matter how small, helps make video editing more accessible - for everyone. -

+function AllContributorsSection({ + contributors, +}: { + contributors: Contributor[]; +}) { + return ( +
+
+

All contributors

+

+ Everyone who makes OpenCut better +

+
+ +
+ {contributors.map((contributor) => ( + +
+ + + + {contributor.login.charAt(0).toUpperCase()} + + +
+

{contributor.login}

+

+ {contributor.contributions} +

+
+
+ + ))} +
+
+ ); +} -
- - - - - - -
-
-
-
-
-
-
- ); +function ExternalToolsSection() { + return ( +
+
+

External tools

+

Tools we use to build OpenCut

+
+ +
+ {EXTERNAL_TOOLS.map((tool, index) => ( + + + + +
+

{tool.name}

+

+ {tool.description} +

+
+
+
+ + ))} +
+
+ ); } diff --git a/apps/web/src/app/editor/[project_id]/layout.tsx b/apps/web/src/app/editor/[project_id]/layout.tsx deleted file mode 100644 index c37e78b7..00000000 --- a/apps/web/src/app/editor/[project_id]/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -export default function EditorLayout({ - children, -}: { - children: React.ReactNode; -}) { - return
{children}
; -} diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 1b53b6b5..e652e928 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -1,459 +1,108 @@ "use client"; -import { useEffect, useRef } from "react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams } from "next/navigation"; import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, } from "@/components/ui/resizable"; -import { MediaPanel } from "@/components/editor/media-panel"; -import { PropertiesPanel } from "@/components/editor/properties-panel"; +import { AssetsPanel } from "@/components/editor/panels/assets"; +import { PropertiesPanel } from "@/components/editor/panels/properties"; import { Timeline } from "@/components/editor/timeline"; -import { PreviewPanel } from "@/components/editor/preview-panel"; +import { PreviewPanel } from "@/components/editor/panels/preview"; import { EditorHeader } from "@/components/editor/editor-header"; -import { usePanelStore } from "@/stores/panel-store"; -import { useProjectStore } from "@/stores/project-store"; import { EditorProvider } from "@/components/providers/editor-provider"; -import { usePlaybackControls } from "@/hooks/use-playback-controls"; import { Onboarding } from "@/components/editor/onboarding"; +import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog"; +import { usePanelStore } from "@/stores/panel-store"; export default function Editor() { - const { - toolsPanel, - previewPanel, - mainContent, - timeline, - setToolsPanel, - setPreviewPanel, - setMainContent, - setTimeline, - propertiesPanel, - setPropertiesPanel, - activePreset, - resetCounter, - } = usePanelStore(); - - const { - activeProject, - loadProject, - createNewProject, - isInvalidProjectId, - markProjectIdAsInvalid, - } = useProjectStore(); - const params = useParams(); - const router = useRouter(); - const projectId = params.project_id as string; - const handledProjectIds = useRef>(new Set()); - const isInitializingRef = useRef(false); - - usePlaybackControls(); - - useEffect(() => { - let isCancelled = false; - - const initProject = async () => { - if (!projectId) { - return; - } - - // Prevent duplicate initialization - if (isInitializingRef.current) { - return; - } - - // Check if project is already loaded - if (activeProject?.id === projectId) { - return; - } - - // Check global invalid tracking first (most important for preventing duplicates) - if (isInvalidProjectId(projectId)) { - return; - } - - // Check if we've already handled this project ID locally - if (handledProjectIds.current.has(projectId)) { - return; - } - - // Mark as initializing to prevent race conditions - isInitializingRef.current = true; - handledProjectIds.current.add(projectId); - - try { - await loadProject(projectId); - - // Check if component was unmounted during async operation - if (isCancelled) { - return; - } - - // Project loaded successfully - isInitializingRef.current = false; - } catch (error) { - // Check if component was unmounted during async operation - if (isCancelled) { - return; - } - - // More specific error handling - only create new project for actual "not found" errors - const isProjectNotFound = - error instanceof Error && - (error.message.includes("not found") || - error.message.includes("does not exist") || - error.message.includes("Project not found")); - - if (isProjectNotFound) { - // Mark this project ID as invalid globally BEFORE creating project - markProjectIdAsInvalid(projectId); - - try { - const newProjectId = await createNewProject("Untitled Project"); - - // Check again if component was unmounted - if (isCancelled) { - return; - } - - router.replace(`/editor/${newProjectId}`); - } catch (createError) { - console.error("Failed to create new project:", createError); - } - } else { - // For other errors (storage issues, corruption, etc.), don't create new project - console.error( - "Project loading failed with recoverable error:", - error - ); - // Remove from handled set so user can retry - handledProjectIds.current.delete(projectId); - } - - isInitializingRef.current = false; - } - }; - - initProject(); - - // Cleanup function to cancel async operations - return () => { - isCancelled = true; - isInitializingRef.current = false; - }; - }, [ - projectId, - loadProject, - createNewProject, - router, - isInvalidProjectId, - markProjectIdAsInvalid, - ]); - - return ( - -
- -
- {activePreset === "media" ? ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : activePreset === "inspector" ? ( - - setPropertiesPanel(100 - size)} - className="min-w-0 min-h-0" - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : activePreset === "vertical-preview" ? ( - - setPreviewPanel(100 - size)} - className="min-w-0 min-h-0" - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - - - {/* Main content area */} - - {/* Tools Panel */} - - - - - - - {/* Preview Area */} - - - - - - - - - - - - - + const params = useParams(); + const projectId = params.project_id as string; + + return ( + +
+ +
+ +
+ + +
+
+ ); +} - {/* Timeline */} - - - -
- )} -
- -
-
- ); +function EditorLayout() { + const { panels, setPanel } = usePanelStore(); + + return ( + { + setPanel("mainContent", sizes[0] ?? panels.mainContent); + setPanel("timeline", sizes[1] ?? panels.timeline); + }} + > + + { + setPanel("tools", sizes[0] ?? panels.tools); + setPanel("preview", sizes[1] ?? panels.preview); + setPanel("properties", sizes[2] ?? panels.properties); + }} + > + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index ca78ee26..d0da1f51 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -8,85 +8,88 @@ @plugin "tailwindcss-animate"; :root { - /* Custom colors - light mode (default) */ - --background: hsl(0, 0%, 100%); - --foreground: hsl(0 0% 11%); - --card: hsl(216, 8%, 86%); - --card-foreground: hsl(0 0% 2%); - --popover: hsl(0, 0%, 100%); - --popover-foreground: hsl(0 0% 2%); - --primary: hsl(205, 84%, 47%); - --primary-foreground: hsl(0 0% 91%); - --secondary: hsl(216, 13%, 92%); - --secondary-foreground: hsl(0 0% 2%); - --muted: hsl(0 0% 85.1%); - --muted-foreground: hsl(0 0% 50%); - --accent: hsl(216, 13%, 92%); - --accent-foreground: hsl(0 0% 2%); - --destructive: hsl(0, 83%, 50%); - --destructive-foreground: hsl(0, 0%, 100%); - --border: hsl(0 0% 83%); - --input: hsl(0 0% 85.1%); - --ring: hsl(0, 0%, 55%); - --chart-1: hsl(220 70% 50%); - --chart-2: hsl(160 60% 45%); - --chart-3: hsl(30 80% 55%); - --chart-4: hsl(280 65% 60%); - --chart-5: hsl(340 75% 55%); - --sidebar-background: hsl(0 0% 96.1%); - --sidebar-foreground: hsl(0 0% 2%); - --sidebar-primary: hsl(0 0% 2%); - --sidebar-primary-foreground: hsl(0 0% 91%); - --sidebar-accent: hsl(0 0% 85.1%); - --sidebar-accent-foreground: hsl(0 0% 2%); - --sidebar-border: hsl(0 0% 85.1%); - --sidebar-ring: hsl(0 0% 16.9%); - --panel-background: hsl(216 13% 92%); - --panel-accent: hsl(216, 8%, 86%); - - /* Radius base */ - --radius: 1rem; + --background: hsl(0, 0%, 100%); + --foreground: hsl(0 0% 11%); + --card: hsl(0, 0%, 100%); + --card-foreground: hsl(0 0% 11%); + --popover: hsl(0, 0%, 100%); + --popover-foreground: hsl(0 0% 2%); + --primary: hsl(203, 100%, 50%); + --primary-hover: hsl(203, 100%, 45%); + --primary-foreground: hsl(0, 0%, 100%); + --secondary: hsl(216 13% 94%); + --secondary-foreground: hsl(0 0% 2%); + --muted: hsl(0 0% 85.1%); + --muted-foreground: hsl(0 0% 50%); + --accent: hsl(216, 13%, 88%); + --accent-foreground: hsl(0 0% 2%); + --destructive: hsl(0, 83%, 50%); + --destructive-foreground: hsl(0, 0%, 100%); + --constructive: hsl(141, 71%, 48%); + --constructive-foreground: hsl(0, 0%, 100%); + --border: hsl(0 0% 88%); + --input: hsl(0 0% 85.1%); + --ring: hsl(0, 0%, 55%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(0 0% 96.1%); + --sidebar-foreground: hsl(0 0% 2%); + --sidebar-primary: hsl(0 0% 2%); + --sidebar-primary-foreground: hsl(0 0% 91%); + --sidebar-accent: hsl(0 0% 85.1%); + --sidebar-accent-foreground: hsl(0 0% 2%); + --sidebar-border: hsl(0 0% 85.1%); + --sidebar-ring: hsl(0 0% 16.9%); + --panel-background: hsl(216 13% 94%); + --panel-accent: hsl(216, 8%, 88%); + --sidebar: hsl(0 0% 98%); } .dark { - /* Custom colors - dark mode */ - --background: hsl(0 0% 4%); - --foreground: hsl(0 0% 89%); - --card: hsl(0 0% 14.9%); - --card-foreground: hsl(0 0% 98%); - --popover: hsl(0 0% 14.9%); - --popover-foreground: hsl(0 0% 98%); - --primary: hsl(205, 84%, 53%); - --primary-foreground: hsl(0 0% 9%); - --secondary: hsl(0 0% 14.9%); - --secondary-foreground: hsl(0 0% 98%); - --muted: hsl(0 0% 14.9%); - --muted-foreground: hsl(0 0% 63.9%); - --accent: hsl(0 0% 14.9%); - --accent-foreground: hsl(0 0% 98%); - --destructive: hsl(0 100% 60%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(0 0% 17%); - --input: hsl(0 0% 14.9%); - --ring: hsl(0 0% 83.1%); - --chart-1: hsl(220 70% 50%); - --chart-2: hsl(160 60% 45%); - --chart-3: hsl(30 80% 55%); - --chart-4: hsl(280 65% 60%); - --chart-5: hsl(340 75% 55%); - --sidebar-background: hsl(0 0% 3.9%); - --sidebar-foreground: hsl(0 0% 98%); - --sidebar-primary: hsl(0 0% 98%); - --sidebar-primary-foreground: hsl(0 0% 9%); - --sidebar-accent: hsl(0 0% 14.9%); - --sidebar-accent-foreground: hsl(0 0% 98%); - --sidebar-border: hsl(0 0% 14.9%); - --sidebar-ring: hsl(0 0% 83.1%); - --panel-background: hsl(0 0% 11%); - --panel-accent: hsl(0 0% 15%); + --background: hsl(0 0% 4%); + --foreground: hsl(0 0% 89%); + --card: hsl(0 0% 4%); + --card-foreground: hsl(0 0% 89%); + --popover: hsl(0 0% 14.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(203, 100%, 50%); + --primary-hover: hsl(203, 100%, 45%); + --primary-foreground: hsl(0 0% 9%); + --secondary: hsl(0 0% 14.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(0 0% 14.9%); + --muted-foreground: hsl(0 0% 63.9%); + --accent: hsl(0, 0%, 28%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 83%, 55%); + --destructive-foreground: hsl(0 0% 98%); + --constructive: hsl(141, 71%, 48%); + --constructive-foreground: hsl(0 0% 100%); + --border: hsl(0 0% 17%); + --input: hsl(0 0% 14.9%); + --ring: hsl(0 0% 83.1%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(0 0% 3.9%); + --sidebar-foreground: hsl(0 0% 98%); + --sidebar-primary: hsl(0 0% 98%); + --sidebar-primary-foreground: hsl(0 0% 9%); + --sidebar-accent: hsl(0 0% 14.9%); + --sidebar-accent-foreground: hsl(0 0% 98%); + --sidebar-border: hsl(0 0% 14.9%); + --sidebar-ring: hsl(0 0% 83.1%); + --panel-background: hsl(0 0% 11%); + --panel-accent: hsl(0 0% 15%); + --sidebar: hsl(240 5.9% 10%); } @layer base { - /* + /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. @@ -94,152 +97,165 @@ If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: var(--color-gray-200, currentcolor); - } - /* Other default base styles */ - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - /* Prevent back/forward swipe */ - overscroll-behavior-x: contain; - } + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + /* Other default base styles */ + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + /* Prevent back/forward swipe */ + overscroll-behavior-x: contain; + } } @theme inline { - /* Responsive breakpoints */ - --breakpoint-xs: 30rem; - - /* Typography */ - --font-sans: var(--font-inter), sans-serif; - - /* Font sizes */ - --text-base: 0.95rem; - --text-base--line-height: calc(1.5 / 0.95); - --text-xs: 0.8rem; - --text-xs--line-height: calc(1 / 0.8); - - /* Border radius */ - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 8px); - - /* Palette mapped to root design tokens */ - --color-background: var(--background); - --color-foreground: var(--foreground); - - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - - /* Chart colors */ - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - - /* Sidebar */ - --color-sidebar: var(--sidebar-background); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); - - /* Panel */ - --color-panel: var(--panel-background); - --color-panel-accent: var(--panel-accent); - - /* Animations */ - --animate-accordion-down: accordion-down 0.2s ease-out; - --animate-accordion-up: accordion-up 0.2s ease-out; - - @keyframes accordion-down { - from { - height: 0; - } - to { - height: var(--radix-accordion-content-height); - } - } - - @keyframes accordion-up { - from { - height: var(--radix-accordion-content-height); - } - to { - height: 0; - } - } + /* Responsive breakpoints */ + --breakpoint-xs: 30rem; + + /* Typography */ + --font-sans: var(--font-inter), sans-serif; + + /* Font sizes */ + --text-xl: 1.20rem; + --text-base: 0.92rem; + --text-base--line-height: calc(1.5 / 0.95); + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.8); + + /* Border radius */ + --radius-lg: 0.82rem; + --radius-md: 0.65rem; + --radius-sm: 0.35rem; + + /* Palette mapped to root design tokens */ + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-primary-hover: var(--primary-hover); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-constructive: var(--constructive); + --color-constructive-foreground: var(--constructive-foreground); + + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + /* Chart colors */ + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + /* Sidebar */ + --color-sidebar: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* Panel */ + --color-panel: var(--panel-background); + --color-panel-accent: var(--panel-accent); + + /* Animations */ + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } } @utility scrollbar-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } } @utility scrollbar-x-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar:horizontal { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:horizontal { + display: none; + } } @utility scrollbar-y-hidden { - -ms-overflow-style: none; - scrollbar-width: none; - &::-webkit-scrollbar:vertical { - display: none; - } + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:vertical { + display: none; + } } @utility scrollbar-thin { - &::-webkit-scrollbar { - width: 6px; - height: 8px; - } - &::-webkit-scrollbar-track { - background: transparent; - } - &::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 4px; - } - &::-webkit-scrollbar-thumb:hover { - background: var(--muted-foreground); - } + &::-webkit-scrollbar { + width: 6px; + height: 8px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; + } + &::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); + } +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 7e32ea22..2b3d0c30 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,62 +1,58 @@ import { ThemeProvider } from "next-themes"; -import { Analytics } from "@vercel/analytics/react"; import Script from "next/script"; import "./globals.css"; import { Toaster } from "../components/ui/sonner"; import { TooltipProvider } from "../components/ui/tooltip"; -import { StorageProvider } from "../components/storage-provider"; -import { ScenesMigrator } from "../components/providers/migrators/scenes-migrator"; import { baseMetaData } from "./metadata"; -import { defaultFont } from "../lib/font-config"; import { BotIdClient } from "botid/client"; -import { env } from "@/env"; +import { webEnv } from "@opencut/env/web"; +import { Inter } from "next/font/google"; + +const siteFont = Inter({ subsets: ["latin"] }); export const metadata = baseMetaData; const protectedRoutes = [ - { - path: "/none", - method: "GET", - }, - { - path: "/api/waitlist/export", - method: "POST", - }, + { + path: "/none", + method: "GET", + }, ]; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - - - - - - - {children} - - - -