Compare commits

...
Author SHA1 Message Date
bab8af831b Merge branch 'rewrite'
Co-authored-by: Maze <mazeincoding@users.noreply.github.com>
2026-07-10 19:57:55 +00:00
b59255cca8 feat(desktop): scaffold desktop app
Co-authored-by: Maze <mazeincoding@users.noreply.github.com>
2026-07-10 19:57:05 +00:00
Maze Winther a2b520fbb6 ci: update workflow for rewrite 2026-06-21 15:14:01 +02:00
Maze Winther a53cd53f55 Merge branch 'rewrite' 2026-06-21 15:09:34 +02:00
Maze Winther 7ae51862b6 chore(tooling): remove root package.json, let moon manage per-project 2026-06-21 14:59:52 +02:00
Maze Winther 6c81db47c0 chore(tooling): replace turbo with moon, add proto toolchain 2026-06-21 14:59:52 +02:00
Maze Winther 8768f6dd8f feat(api): add elysia server for cloudflare workers 2026-06-21 14:59:52 +02:00
Maze Winther 107d7ab714 chore: gitignore 2026-06-14 21:05:15 +02:00
Maze Winther b315020416 feat: add hello world to home page 2026-05-28 00:51:29 +02:00
Maze Winther a5888e2087 chore: remove architecture reference from package.json 2026-05-28 00:37:25 +02:00
Maze Winther b601ab4b24 docs: fix readme conflict markers 2026-05-27 22:34:49 +02:00
Maze Winther 6482461a63 docs: use readme from origin/main 2026-05-27 22:29:47 +02:00
Maze Winther f402fb8337 chore: replace codebase with rewrite 2026-05-27 22:27:11 +02:00
Maze Winther 446e68e65e chore: monorepo setup and web foundation 2026-05-27 00:30:10 +02:00
Maze Winther 238750c025 docs: refocus readme on rewrite 2026-05-18 17:06:40 +02:00
Senthil Raja RandGitHub 6fdb1559aa feat: display keyboard shortcuts on toolbar buttons (#788)
- Add shortcut hints to toolbar buttons via tooltip text
- Integrate useKeyboardShortcutsHelp hook for consistent shortcut retrieval
- Timeline toolbar: split, duplicate, delete, snapping, bookmark buttons
- Preview toolbar: play/pause button
- Format: tooltip text appended with '(Key)' or '(Key1 or Key2)'
- Gracefully handle actions without defined shortcuts
2026-05-17 04:27:16 +02:00
Maze Winther 3f2d5cfcaa chore: init 2026-05-09 00:42:26 +02:00
Maze Winther b3d35fbc2a chore: clean state 2026-05-06 22:59:08 +02:00
MazeandGitHub fbe3db74d7 chore: update copyright year 2026-05-06 22:53:51 +02:00
Maze Winther d1f4cb615b docs: fix readme 2026-05-02 16:51:01 +02:00
Maze Winther 19f1086492 Merge branch 'domain-import' 2026-05-02 16:46:13 +02:00
Maze Winther 8ab3aeba7c docs: move NOTES.md file 2026-05-02 16:38:06 +02:00
Maze Winther 6423b352d6 chore: ignore migrations file 2026-05-02 16:26:09 +02:00
Maze Winther 7b942b363d chore: remove file 2026-05-02 03:27:23 +02:00
Maze WintherandCursor 8b8d65b509 refactor: collapse mask definition registry to a single keyed registry
The freeform mask was assembled on every getMaskDefinition("freeform") call,
re-spreading params and injecting a wrong SquareIcon placeholder, with a type
cast to suppress the structural mismatch. Root cause: MasksRegistry was keyed
on BuiltinMaskType, so freeform had no entry.

- Re-key MasksRegistry from BuiltinMaskType to MaskType
- Register freeform alongside builtins in registerDefaultMasks, with
  PenToolAddIcon as its menu icon
- getMaskDefinition and getMaskDefinitionsForMenu become plain registry
  lookups with no branching, no manual spreading, no cast
- Rename RegisteredBuiltinMaskDefinition to RegisteredMaskDefinition and
  tighten computeParamUpdate return type to Partial<BaseMaskParams>

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 03:27:23 +02:00
Maze WintherandCursor 23187b0b6a refactor: remove dead tangent handle variant
The { kind: "tangent" } MaskHandleId variant was never constructed — neither
getFreeformDisplayHandles nor any other code ever produced a tangent handle
object. The in/out control-arm branches in computeFreeformParamUpdate, the
tangent case in maskHandleIdKey, and the CUSTOM_MASK_TANGENT_SIZE rendering
path in mask-handles.tsx were all unreachable. Removed all four dead paths
and removed "tangent" from MaskHandleKind.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 03:27:23 +02:00
Maze WintherandCursor f66cdbfeb2 fix: address review findings in masks substrate
- Remove dead ternary in getPreferredEdges (always returned undefined; the
  all-false object was never reached by Object.values().some(Boolean))
- Document why drawWithFeather descriptors zero the feather field in the
  compositor (analytical renderer handles softness itself; zeroing prevents
  a second JFA feather pass)
- Remove legacyType from V30→V31 migration (not consumed anywhere; a
  down-migration can map type:"freeform" back to "custom" directly)
- Add V30→V31 test coverage: skip-guard branches, freeform passthrough,
  elements without a masks array
- Add snap tests for vertical-edge (bottom) and uniform-scale handle paths

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 03:27:23 +02:00
Maze Winther 99137fe493 docs: capture mask rust migration notes
Made-with: Cursor
2026-05-02 03:27:23 +02:00
Maze Winther 79dcec8a2e refactor: split builtin and freeform masks
Made-with: Cursor
2026-05-02 03:27:23 +02:00
Maze Winther 97f8d1968f refactor: type mask handle identifiers
Made-with: Cursor
2026-05-02 03:27:23 +02:00
Maze Winther ae1292a14b refactor: make mask renderer dispatch explicit
Made-with: Cursor
2026-05-02 03:27:23 +02:00
Maze Winther 6ea5a156b6 refactor: split mask snap dispatch
Made-with: Cursor
2026-05-02 03:27:23 +02:00
Maze Winther 401cc35743 refactor: animate params over time 2026-05-02 03:27:23 +02:00
Maze Winther b8d08df244 chore: cleanup 2026-05-02 03:27:23 +02:00
Maze Winther 2ee0a44735 refactor: unify element params 2026-05-02 03:27:23 +02:00
Maze Winther a9b9cf6bf5 chore: switch from biome to eslint + prettier; fix ton of lint issues 2026-05-02 03:27:23 +02:00
Maze Winther d6622dc6b3 chore: untrack cargo-tools files 2026-05-02 03:10:08 +02:00
Maze Winther 6ec818fc11 chore: remove accidental file
Made-with: Cursor
2026-04-26 21:31:12 +02:00
Maze Winther 6daa69635b fix: restore README header, badges, links, and star chart
Made-with: Cursor
2026-04-26 20:51:32 +02:00
Maze Winther 6b1f38a9a3 refactor: split animation helpers by domain 2026-04-26 20:44:07 +02:00
Maze Winther 56ca09692a refactor: thinner hooks 2026-04-26 17:31:28 +02:00
Maze Winther 0b7597b31f refactor: split animation helpers by domain 2026-04-26 04:04:30 +02:00
Maze Winther e6c6193638 fix: linter 2026-04-26 03:06:13 +02:00
Maze Winther 685d902740 fix: follow-up to prev commit 2026-04-26 02:59:30 +02:00
Maze Winther eea6d43c82 fix: model editor time as wasm MediaTime ticks end-to-end 2026-04-26 02:50:04 +02:00
Maze Winther 6a22a3ecbd refactor: centralize editor interaction state
Move timeline and preview gestures into dedicated controllers so hooks stay thin around editor state and DOM events.

This also routes drag and playback synchronization through manager APIs and reuses persistent wasm surfaces so scrubbing, seeking, and preview rendering stay in sync.

Made-with: Cursor
2026-04-25 03:56:22 +02:00
Maze Winther 0dffefdd59 fix: surface unsupported video codec on import, fix Input leaks 2026-04-25 02:46:31 +02:00
Maze Winther cceb835a84 perf: improve rendering performance by ~20x 2026-04-22 16:48:37 +02:00
Maze Winther f1b45b4328 chore: remove stale files 2026-04-22 14:36:51 +02:00
Maze Winther 7428dc75cf chore: bump opencut-wasm version 2026-04-22 14:24:28 +02:00
Maze Winther 1301d027e9 fix: wasm WebGL only until WebGPU is fixed 2026-04-22 14:20:40 +02:00
Maze Winther f5f31b43df refactor: migration run() and pure v1-to-v2 transform 2026-04-22 02:41:18 +02:00
Maze Winther a709af0059 chore: formatting 2026-04-22 02:05:41 +02:00
Maze Winther 75eebf52bc fix: project loading thing 2026-04-21 20:45:33 +02:00
Maze Winther 1582423dbf chore: delete ffmpeg public assets 2026-04-21 13:00:26 +02:00
Maze Winther 148d38d7c9 docs: ts conventions to agents.md 2026-04-21 12:47:21 +02:00
Maze Winther 3e89d29985 refactor: restructure files to their domains, new preview overlay system, and dep graph 2026-04-20 11:31:17 +02:00
Maze Winther 729d10592f fix: WebGL frame rendering and opencut-wasm 0.2.8 2026-04-19 07:42:52 +02:00
Maze Winther 9da365bc01 docs: add prerequisites for rust 2026-04-19 07:23:15 +02:00
Maze Winther 8bdc894690 fix: migrate commands to CommandResult selection contract 2026-04-18 16:01:35 +02:00
Maze Winther 0387bd4c80 delete legacy folder 2026-04-18 15:35:04 +02:00
Maze Winther b1d0b10354 fix: default audio waveform color from timeline theme 2026-04-18 15:11:52 +02:00
Maze Winther 94eed96cad chore: update opencut-wasm version to 0.2.6 in Cargo.lock 2026-04-18 15:03:00 +02:00
Maze Winther 794d4ae149 fix: align LayerUniformBuffer to 16 bytes for WebGL compat 2026-04-18 15:01:59 +02:00
Maze Winther 011f51051c feat: clip volume line and timeline waveform improvements 2026-04-18 01:40:18 +02:00
Maze Winther c46d28891f feat: text / custom masks, fix preview pan, refactor selection 2026-04-15 15:45:50 +02:00
Maze Winther 66e4367bc4 feat: history view in feedback popover 2026-04-15 06:39:52 +02:00
Maze Winther 8161de00e6 chore: formatting 2026-04-15 06:26:55 +02:00
Maze Winther eb28942dab fix: audio waveform height in timeline 2026-04-15 06:05:13 +02:00
Maze Winther 68f175d3be fix: correct schema path in drizzle config 2026-04-15 06:04:29 +02:00
Maze Winther d5c1b7bf0e fix: load dotenv before env validation in drizzle config 2026-04-15 06:03:23 +02:00
Maze Winther 0868d9cf13 tests: delete 2026-04-15 05:53:06 +02:00
Maze Winther 8a3577d2e7 review: fixes 2026-04-15 05:52:33 +02:00
Maze Winther ac43a5a73f feat: move/resize multiple elements 2026-04-15 05:43:29 +02:00
Maze Winther 6312fc5b07 fix: repair scene tracks broken by v23-to-v24 migration 2026-04-15 04:45:15 +02:00
Maze Winther 2da93aa98f changelog: rephrasing 2026-04-15 04:34:36 +02:00
Maze Winther f4bd689f51 chore: force deployment 2026-04-15 03:59:52 +02:00
Maze Winther 4f7dc5ee3a fix: remap split-element keybinding to split 2026-04-15 03:57:51 +02:00
Maze Winther 4e8b7d3745 fix: skip unknown actions in keyboard shortcuts help 2026-04-15 03:47:57 +02:00
Maze Winther ee372266ca chore: force deployment 2026-04-15 03:35:07 +02:00
Maze Winther e10b0a3355 tests: remove 2026-04-15 02:44:52 +02:00
Maze Winther 3914ea9b71 fix: show changelog notice when stored version is missing or older 2026-04-15 02:13:42 +02:00
Maze Winther 370944f443 feat: new-release notice and colocate changelog under domain 2026-04-15 02:05:41 +02:00
Maze Winther c839db9234 fix: remove redundant "opencut" prefix from storage key 2026-04-15 01:50:30 +02:00
Maze Winther 769a7e708a changelog: another accuracy fix 2026-04-15 01:02:12 +02:00
Maze Winther 1fa5442f21 chore: remove debugging throughout codebase 2026-04-15 01:00:05 +02:00
Maze Winther dc0a8e7c96 feat: graph editor supports multiple properties at once 2026-04-15 00:59:54 +02:00
Maze Winther 3db30c72e7 fix: turbo 2026-04-15 00:41:52 +02:00
Maze Winther eca13e3e71 changelog: update to be more accurate 2026-04-15 00:40:22 +02:00
Maze Winther 1943a4d18f feat: changelog now supports markdown within entries 2026-04-15 00:33:59 +02:00
Maze Winther 3eccc2f9e3 fix: copy to markdown formatting 2026-04-15 00:29:04 +02:00
Maze Winther 368a20ddc2 changelog: fix date 2026-04-15 00:26:12 +02:00
Maze Winther 64588ceeea docs: fill in v0.3.0 technical changelog entries 2026-04-15 00:22:20 +02:00
Maze Winther 8fe66090e7 feat: add support for technical details in changelog entries 2026-04-14 23:55:20 +02:00
Maze Winther 91e1232112 feat: transcription diagnostics 2026-04-14 23:34:57 +02:00
Maze Winther f726d95295 chore: add .tokeignore 2026-04-14 22:10:15 +02:00
Maze Winther 78a0a27a72 feat: editor feedback 2026-04-14 21:45:11 +02:00
Maze Winther b68bf00454 docs: formatting 2026-04-14 20:49:17 +02:00
Maze Winther cd19a4409f docs: update file structure to be accurate 2026-04-14 20:47:30 +02:00
Maze Winther f1ed84006a docs: remove features section 2026-04-14 20:43:18 +02:00
Maze Winther 8d82ea9c0b feat: draft persistence on Form via persistKey 2026-04-14 19:13:06 +02:00
Maze Winther 6aa4ffe462 style: export button icon size 2026-04-14 18:38:59 +02:00
Maze Winther 8a54e5d884 test: remove keyframe query 2026-04-14 18:34:33 +02:00
Maze Winther 5e6c26f3c1 fix: keyframe removal state 2026-04-14 18:32:07 +02:00
Maze Winther 520c0b3264 chore: remove unused import 2026-04-14 18:30:34 +02:00
Maze Winther 607eaeacc7 fix: timeline element resizing 2026-04-14 18:29:23 +02:00
Maze Winther 6de6c7fd95 refactor: move constants to domain modules 2026-04-14 18:17:14 +02:00
Maze Winther 34c67ef8e8 chore: fix comment 2026-04-14 15:27:24 +02:00
Maze Winther 5f2f00dde0 chore: hide logos sticker category 2026-04-14 15:14:30 +02:00
Maze Winther 6db7ad0696 chore: hide preview grid guide until v0.4.0 2026-04-14 15:08:06 +02:00
Maze Winther 24a0d10a92 changelog 2026-04-14 14:57:40 +02:00
e3d4f3fa92 Add support for basic SRT subtitle import to Captions panel (#756)
* Add SRT subtitle import to the captions panel

[Modified from 0bd641b4102c827ef8d218ada099229f73396077, 2026-03-28 08:46:23 -0400]

Parse .srt files into chunks and insert them as text elements on a new text track using the existing caption styling defaults. Displays a warning when malformed cues are skipped during import.

* Batch caption text insertion

[Modified from eece3bbc5f0578a24a4f90f0ab87fe5ca37e94e9, 2026-03-28 09:13:18 -0400]

Batch caption insertion in a BatchCommand so that undo will batch undo all subtitle insertion from either srt file or ai transcriptions.

* Move subtitle parsing into a shared interface

[Modified from e84404b7857f8c611eda7a65b5d61ee6c9823d75, 2026-03-28 09:52:31 -0400]

Extracted the srt specific extraction behind a generic shared interface to allow easier adding of other subtitle formats in the future.

* Apply better default styling for subtitles.

[Modified from b5deec4285f99873ba5bf57971f7bcab26e8c743, 2026-03-31 11:49:34 -0400]

Shrink font size, aim to layout 5% above bottom, keep subtitle width 80% of video, wrap text on new lines if too long.

* Update Captions Panel UI

Split captions panel into Generate and Import tabs as they are two different approaches for adding captions.

* Add biome formatting fixes

* Add .ass subtitle import support

Extend subtitle import to accept .ass files alongside .srt, map supported .ass styles onto imported text elements, and display warnings when unsupported .ass features are skipped.

* Change “Import subtitles” button style

* refactor: review fixes

* fix: convert subtitle times from seconds to ticks

* refactor: replace captions tab switcher with import action button

---------

Co-authored-by: Maze Winther <mazewinther@gmail.com>
2026-04-14 03:19:12 +02:00
Maze Winther d85566ab8d changelog: publish 0.3.0 2026-04-14 00:50:18 +02:00
Maze Winther 3191e038d8 changelog 2026-04-14 00:33:28 +02:00
Maze Winther ba994d8bf9 fix: linter 2026-04-13 22:00:29 +02:00
Maze Winther 270e2951b1 feat: graph editor for flat keyframe segments 2026-04-13 21:59:50 +02:00
Maze Winther 496f7d690d chore: remove filters tab from assets panel 2026-04-13 21:56:47 +02:00
Maze Winther 11ee0dbf27 fix: stable background blur previews in settings 2026-04-13 21:56:18 +02:00
Maze Winther 2c344fa01d fix: use published opencut-wasm package 2026-04-13 05:08:29 +02:00
Maze Winther 3d9d669cb9 chore: bump opencut-wasm to 0.2.5 2026-04-13 05:08:29 +02:00
Maze Winther 7feb3b238c changelog: 0.3.0
Made-with: Cursor
2026-04-13 05:08:29 +02:00
Maze Winther 7d4e166678 fix: text measurement, rotation handle, video cache races, playback sync
Made-with: Cursor
2026-04-13 05:08:29 +02:00
Maze Winther cad88ee4d1 refactor: rendering pipeline
Made-with: Cursor
2026-04-13 05:08:29 +02:00
Maze Winther 36efdf66e7 feat: expandable keyframe lanes in timeline
Made-with: Cursor
2026-04-13 05:08:29 +02:00
Maze Winther 2272598d42 feat: clipboard with keyframe paste
Made-with: Cursor
2026-04-13 05:03:10 +02:00
Maze Winther 5a2aaf3601 feat: cinematic bars, diamond, heart, and star masks
Made-with: Cursor
2026-04-13 05:01:51 +02:00
Maze Winther 108d3f4eca feat: position animated independently per axis
Made-with: Cursor
2026-04-13 05:01:51 +02:00
Maze Winther 8230faf082 feat: wasm compositor
Made-with: Cursor
2026-04-13 05:01:51 +02:00
Maze Winther dbd68ff139 refactor: gpu webgl fallback and batched command encoding
Made-with: Cursor
2026-04-13 05:01:51 +02:00
Maze Winther 0d0d737563 fix: one more 2026-04-13 05:01:51 +02:00
Maze Winther f06a930fc1 fix: convert media asset duration to ticks on insert 2026-04-13 05:01:51 +02:00
Maze Winther 532f050a64 changelog: add entry 2026-04-13 05:00:19 +02:00
Maze Winther d422b602c9 fix: clicking a selected element collapses multi-selection 2026-04-13 05:00:19 +02:00
Maze Winther cbd1b82123 refactor: structured track layout per scene 2026-04-07 04:41:27 +02:00
Maze Winther 4f7d401a97 style: compact dropdown and context menu styling 2026-04-07 01:55:21 +02:00
Maze Winther 30709d7134 fix: fix timeline width calculation for tick-based time 2026-04-07 01:45:52 +02:00
Maze Winther 93a0afdf64 fix: use line-height-based bounds for text elements 2026-04-07 01:39:35 +02:00
Maze Winther b8979c65d4 feat: select element on insert 2026-04-07 01:20:07 +02:00
Maze Winther f47e85c3bb fix: respect scale lock when editing keyframed scale 2026-04-07 01:19:46 +02:00
Maze Winther e43b859439 fix: place new overlay tracks at the top of the timeline 2026-04-07 01:18:57 +02:00
Maze Winther e4b67094e7 refactor: split effects and masks into dedicated rust crates, introduce MediaTime and FrameRate 2026-04-07 01:09:13 +02:00
Maze Winther 79df736431 fix: clean up graph editor 2026-04-06 13:15:50 +02:00
Maze Winther c411edb7c1 fix: clean up 2026-04-06 12:59:07 +02:00
Maze Winther 2776adaae1 fix: whitespace 2026-04-06 12:42:46 +02:00
Maze Winther 964ac82d5e refactor: split presets into data definitions and storage store 2026-04-06 12:40:47 +02:00
Maze Winther 05243616ea refactor: use useShiftKey hook in bezier graph 2026-04-06 01:51:09 +02:00
Maze Winther ccd552a1c0 chore: addition to last merge 2026-04-06 01:24:09 +02:00
Maze Winther fcc09e80e2 Merge branch 'temp' 2026-04-06 01:23:47 +02:00
Maze dedb13546a feat: graphs popover 2026-04-06 01:13:14 +02:00
Maze Winther c56d6f8188 fix: preserve negative start times during timeline drag 2026-04-06 00:57:54 +02:00
Maze Winther e62227914a docs: document rust/apps architecture in AGENTS.md 2026-04-06 00:24:21 +02:00
Maze Winther 44d8c4c876 refactor: unify element updates into pipeline and centralize ripple 2026-04-06 00:02:23 +02:00
Maze Winther 20235d0724 chore: remove comment 2026-04-05 19:04:39 +02:00
Maze 0997466331 fix: subscribe to playback time so keyframe button updates on frame change
Made-with: Cursor
2026-04-05 13:37:29 +02:00
Maze 1bd9fac203 refactor: rewrite animation system to use bindings and scalar channels 2026-04-05 13:15:58 +02:00
Maze 69da5bee0c style: tabs 2026-04-04 11:59:04 +02:00
Maze cb99925d75 refactor: swap out manual tabs withthe shared tabs component in stickers 2026-04-04 11:58:55 +02:00
Maze 78c6c497f1 chore: remove transcription envs 2026-04-04 08:53:10 +02:00
Maze Winther 22725f71fa chore: update opencut-wasm to 0.1.3 2026-04-03 03:37:41 +02:00
Maze Winther 1a8a1e889a refactor: update command execute methods to return CommandResult | undefined 2026-04-03 03:13:49 +02:00
Maze Winther 3516bd69f4 fix: use published opencut-wasm package 2026-04-01 21:30:26 +02:00
Maze Winther 57fc0fd323 chore: sort root package.json scripts 2026-04-01 19:29:12 +02:00
Maze Winther bd9ec023ee refactor: split constants by domain and isolate timeline panel from core 2026-04-01 19:27:49 +02:00
Maze Winther f4b9f40ab1 fix: update tooltip text for graph editor button 2026-04-01 18:17:23 +02:00
Maze Winther 93cc5d50f3 fix: don't crash when GPU isn't available 2026-04-01 18:13:27 +02:00
Maze Winther 8a1d37cf3e fix: align background blur with effect blur intensity scale 2026-04-01 15:15:41 +02:00
Maze Winther e579ae1202 feat: migrate GPU renderer from WebGL to wgpu/WASM 2026-04-01 13:57:32 +02:00
Maze Winther b048b739bd Merge branch 'refactor-selection-commands' 2026-03-31 23:40:59 +02:00
Maze Winther dc82826951 refactor: move track pruning into a post-command reactor 2026-03-31 23:39:43 +02:00
Maze Winther 608dc1a62f Merge branch 'feature/separate-audio' 2026-03-31 23:24:33 +02:00
Maze Winther f1d5932c54 feat: separate audio from elements 2026-03-31 23:23:36 +02:00
Maze Winther cc150c7cfb Merge branch 'refactor-selection-commands' 2026-03-31 22:54:06 +02:00
Maze Winther f8fb238b22 feat: centralize selection management 2026-03-31 22:53:16 +02:00
Maze Winther 0488a6be45 fix: normalize vertical drag direction in preferIndex placement 2026-03-31 22:38:56 +02:00
Maze Winther 0f5dca4f12 fix: build wasm before bun install in CI 2026-03-31 19:02:17 +02:00
Maze Winther 154b78c7d1 fix: allow timeline paste and duplicate commands to reassign updated tracks 2026-03-31 18:47:45 +02:00
Maze Winther d23be079cc Merge branch 'feature/track-placement' 2026-03-31 15:53:23 +02:00
Maze Winther 70a6d58b84 refactor: centralize track placement into its own module 2026-03-31 15:47:30 +02:00
Maze Winther dcf54c1250 feat: migrate time utilities from typescript to rust WASM 2026-03-31 04:03:52 +02:00
Maze Winther 5cc6abc3a4 style: swap ripple icon 2026-03-29 22:31:26 +02:00
Maze Winther 77a14b1023 refactor: standardize icon component props 2026-03-29 22:30:11 +02:00
Maze Winther 1c3b22edec feat: add keyframe graphs button 2026-03-29 21:48:33 +02:00
Maze Winther 41562baeb5 style: remove unnecessary margin from keyframe 2026-03-29 21:48:08 +02:00
Maze Winther df207e0f83 refactor: name magic numbers and rename el in landing handlebars 2026-03-29 21:18:07 +02:00
Maze Winther dc5956900e fix: eliminate first-paint flicker in landing handlebars 2026-03-29 20:45:48 +02:00
Maze Winther b095862cc0 fix: improve DATABASE_URL validation in web environment schema 2026-03-29 18:58:35 +02:00
Maze Winther 0a6ede73ae chore: update deps 2026-03-29 18:57:05 +02:00
Maze Winther 7ce0018c9b chore: update .gitignore 2026-03-29 18:55:56 +02:00
Maze Winther 9528d0053b chore: untrack files 2026-03-29 18:55:10 +02:00
Maze Winther 689853312d fix: remove OpenNext Cloudflare initialization 2026-03-29 18:35:58 +02:00
Maze Winther 5a1d9ef3f4 chore: remove unused middleware file 2026-03-29 18:30:16 +02:00
Rayan SalhabGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Mazecoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
eb66e03f0e fix(security): replace new Function() with safe math expression parser (#753)
* fix(security): replace new Function() with safe math expression parser

Replace the dangerous new Function() constructor in evaluateMathExpression()
with a recursive descent parser that safely evaluates basic arithmetic
expressions.

The Function constructor is a security anti-pattern that could enable
arbitrary code execution if the validation regex is ever bypassed or
relaxed. The new parser:

- Only supports numbers, +, -, *, /, parentheses, and whitespace
- Has no eval-like functionality that could execute arbitrary code
- Maintains backward compatibility with existing expressions
- Handles operator precedence and parentheses correctly

Fixes #725

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Maze <167211895+mazeincoding@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-29 18:27:05 +02:00
Maze Winther a2418751e6 chore: remove accidentally committed Zone.Identifier stream 2026-03-29 18:25:43 +02:00
Maze Winther 5777a555f6 feat: add shared Rust crates for cross-platform time utilities
Introduce `bridge` proc-macro crate with `#[export]` attribute that conditionally applies `wasm_bindgen` for WASM builds, and `time` crate porting all timecode/frame utilities from TypeScript to Rust.
2026-03-29 15:58:46 +02:00
Maze Winther 220ec757d4 refactor: move packages sub-folders into web app 2026-03-29 15:58:46 +02:00
Maze Winther 585e28d49b feat: add published status to changelog entries 2026-03-29 15:58:46 +02:00
Maze Winther 1261e663e1 docs: rewrite agents file 2026-03-29 15:58:46 +02:00
Maze Winther c34e01c63e refactor: clean up number formatting and step handling in property tabs 2026-03-29 15:58:46 +02:00
Maze Winther 3e75852ffe chore: remove dead songs tab 2026-03-29 15:58:46 +02:00
Maze Winther e95992b406 chore: add missing open-next config 2026-03-29 15:58:46 +02:00
Maze Winther 6df9d70b9b chore: update dep 2026-03-29 15:58:46 +02:00
Maze Winther f8a0390827 chore: rename proxy to middleware 2026-03-29 15:58:46 +02:00
Maze Winther d862e56e34 feat: init desktop app 2026-03-29 15:58:42 +02:00
Maze Winther d08fbd73e1 fix: add missing file 2026-03-29 15:57:05 +02:00
Maze Winther 4d127a038b chore: normalize line endings 2026-03-29 15:56:48 +02:00
Maze Winther 82817cb50c chore: next-env 2026-03-29 15:49:30 +02:00
Maze Winther a54b3ef076 chore: update botid 2026-03-29 15:49:30 +02:00
Maze Winther 6dd8191f4b chore: init cloudflare 2026-03-29 15:49:30 +02:00
Maze Winther 722dcdd88f fix: resolve typescript/lint errors 2026-03-29 15:49:30 +02:00
Maze Winther 8db3bead13 feat: masks, properties refactor, shaders, storage migrations, and more 2026-03-29 15:48:22 +02:00
Maze Winther 39ea298a9c chore: gitignore 2026-03-29 15:46:35 +02:00
Maze Winther 2c0fd4f86b untrack .cursor/ 2026-03-29 15:46:35 +02:00
Maze Winther 5e2b829f8a changelog: add already-completed item 2026-03-29 15:46:35 +02:00
Maze Winther b09635064a fix: assets having unreasonable widths and heights 2026-03-29 15:41:10 +02:00
Maze Winther 6018efc5ac changelog: new version with already-made changes 2026-03-29 15:41:10 +02:00
AspenandGitHub 7e2a913e13 Update OpenCut logo in README (#741) 2026-03-28 19:07:36 +01:00
kcfancherandGitHub 63496d05a2 Fix transcription sample-rate handling (#740)
Fix for #703.

Adds an optional sampleRate parameter to the decodeAudioToFloat32() function. The caption transcriber passes in a value of 16000 to prepare audio in the format Whisper expects.
2026-03-28 19:06:40 +01:00
Maze Winther f2eed05d36 fix blog build when marble cms isn't available 2026-03-24 14:18:39 +01:00
Maze Winther 26d523ebad fix: fallback to opus codec 2026-03-09 00:10:31 +01:00
Maze Winther 0be9275a2d dialog: decrease gap 2026-03-04 17:18:28 +01:00
Maze Winther a2d08719f9 refactor: remove foreground variant from button, make it default 2026-03-04 17:16:01 +01:00
Maze Winther ca3e5c98e6 add context menu (w/ brand options) to header logo
allows downloading brand assets directly from the logo in the header
2026-03-04 17:11:32 +01:00
Maze Winther 5ca2de25c6 fix: auto scroll on link 2026-03-04 16:53:04 +01:00
Maze Winther ed8815c755 checker pattern for brand asset cards (branding page) 2026-03-04 16:45:03 +01:00
Maze Winther 24dac1232c rename branding page to brand 2026-03-04 02:56:08 +01:00
Maze Winther 46e55674de feat: branding page 2026-03-04 01:48:39 +01:00
Maze Winther 8e8f1c3596 chore: upgrade tailwind-related deps 2026-03-04 01:47:40 +01:00
Maze Winther 4e0483e425 replace opencut assets 2026-03-03 14:39:56 +01:00
Maze Winther 31dc78637f fix: outline button styling 2026-03-03 14:39:17 +01:00
Maze Winther 705faea56f feat: add changelog link to footer 2026-03-03 14:24:04 +01:00
Maze Winther 21f12ff763 button color 2026-03-03 14:22:14 +01:00
Maze Winther 719e0d4398 fix: invert logos in /sponsors when dark mode 2026-03-03 07:01:17 +01:00
Maze Winther df518cbf4e close effects after last effect removal + spacing 2026-03-02 21:00:28 +01:00
Maze Winther d9a214ef07 fix content-collections implicit content deprecation 2026-03-02 20:50:07 +01:00
Maze Winther dc58c10e64 changelog: fix date 2026-03-02 20:37:42 +01:00
Maze Winther 8fd9b96f80 fix changelog opengraph image 2026-03-02 20:36:28 +01:00
Maze Winther 5216f4398f changelog open graph image 2026-03-02 18:37:04 +01:00
Maze Winther 3a2ec3c291 changelog: improve phrasing for timeline clip gap description 2026-03-02 18:31:37 +01:00
Maze Winther a87b344ba5 Merge branch 'dev' 2026-03-02 17:48:39 +01:00
Maze Winther a635ae9e14 fix inconsistency in blur intensity 2026-03-02 17:43:42 +01:00
Maze Winther 88bf59d6b4 fix effects button in light mode 2026-03-02 17:42:11 +01:00
Maze Winther d62eb6aa9e Merge branch 'main' of https://github.com/OpenCut-app/OpenCut 2026-03-02 17:33:01 +01:00
Maze Winther fbb00cf9cc style: hover of copy markdown button 2026-03-02 17:29:43 +01:00
Maze Winther a63cdcfe7e changelog: update description 2026-03-02 17:28:19 +01:00
Maze Winther 1b870fd9a0 changelog: fix phrasing 2026-03-02 17:15:23 +01:00
Maze Winther c7a00b6af1 feat: copy changelog release as markdown 2026-03-02 17:03:23 +01:00
Maze Winther 9b6a1f7973 disable react-scan in prod 2026-03-02 16:53:30 +01:00
Maze Winther eee3233c97 fix linter 2026-03-02 16:50:55 +01:00
Maze Winther 14ae9b4f11 remove z-index 2026-03-02 16:43:43 +01:00
Maze Winther 565d2595da clcik to seek on keyframes 2026-03-02 16:40:41 +01:00
Maze Winther a526066b61 feat: wire ui to have keyframes for: opacity, text color, and all background properties 2026-03-02 16:36:10 +01:00
Maze Winther b0165da764 fix: selecting multiple effects didn't work 2026-03-02 16:30:50 +01:00
Maze Winther 228e2e4342 docs: keyframes system 2026-03-02 16:02:32 +01:00
Maze Winther 81e7a7fb32 feat: implement keyframe support for more properties at the core (no ui yet) 2026-03-02 15:08:21 +01:00
Maze Winther 887d1795dd fix: keyframe drag bug 2026-03-02 15:06:31 +01:00
Maze Winther 9261589ff3 fix: keyframe placement 2026-03-02 15:06:01 +01:00
Maze Winther db50b5214d feat: ripple support for element resize 2026-03-02 13:26:40 +01:00
Maze Winther e7dcb586c0 feat: Clip effects, asset sorting, and timeline improvements
Major features and improvements:

* **Clip Effects**:
  * Added UI in Properties Panel to manage effects on video/image clips (add, remove, toggle, reorder).
  * Implemented dynamic parameter fields for effects.
  * Added support for keyframing effect parameters.

* **Assets Panel**:
  * Added sorting options: Name, Type, Duration, and File Size.
  * Persisted view preferences (grid/list mode, sort order) to local storage.
  * Refactored media item rendering and drag interactions.

* **Timeline & Interaction**:
  * **Keyframe Dragging**: Added ability to drag keyframes directly on the timeline element.
  * **Resizing**: Improved resize logic to respect neighboring clips (prevents overlaps).
  * **Visuals**: Implemented tiled background rendering for video/image clips on the timeline.
  * **Shortcuts**: Added "Deselect All" action bound to the `Escape` key.
  * **Fixes**: Corrected drag-and-drop coordinate calculations when the timeline track area is scrolled.

* **Text Elements**:
  * Refactored text background storage to use an explicit `enabled` flag.
  * Added `V8toV9` storage migration to update existing projects.

* **Architecture**:
  * Moved export state management to `ProjectManager` for better lifecycle handling.
  * Refactored `PropertiesPanel` sections to be more composable (custom headers, borders).
2026-03-02 13:13:07 +01:00
Maze Winther 93bea01c9e docs: update actions 2026-03-02 10:37:44 +01:00
Maze Winther a2727f2ae5 fix button color 2026-03-02 10:36:37 +01:00
Maze Winther 82f8f76a84 feat: mobile gate 2026-03-02 10:36:19 +01:00
Maze Winther 2074dde3e3 feat: add roadmap link to header 2026-03-02 10:34:54 +01:00
Maze Winther feaeec8dbe feat: add react-scan 2026-03-02 10:34:23 +01:00
Maze Winther 3b74cddbfb fix: global.css 2026-03-02 10:34:05 +01:00
Maze Winther a0695dadaa fix: make roadmap page responsive 2026-03-02 10:33:52 +01:00
Maze Winther 7bd0b43746 update cursor rule 2026-03-02 10:32:46 +01:00
Maze Winther 77a3708213 web: refactor changelog to support a dedicated page for each version + links for each release 2026-03-02 00:55:08 +01:00
Maze Winther 9a00e8ac5b disable cursor rules 2026-03-01 21:48:37 +01:00
Maze Winther 51234e673b changelog: correct release date for version 0.2.0 2026-03-01 17:56:13 +01:00
Maze Winther 13237a04fd changelog: update ripple editing description for clarity in 0.2.0 2026-03-01 01:52:01 +01:00
Maze Winther 35dd209b4e docs: add ripple editing to 0.2.0 changelog 2026-03-01 01:44:38 +01:00
Maze Winther 5e7c27f542 Merge branch 'dev' into ripple-editing 2026-03-01 01:32:09 +01:00
Maze Winther 354f4dec67 changelog: update titles for no title case 2026-03-01 01:12:57 +01:00
Maze Winther c6bffaa243 fix(audio): stabilize playback startup when timeline selection is heavy 2026-03-01 01:10:56 +01:00
Maze Winther f3af18e0c7 delete CHANGELOG.md 2026-03-01 01:08:26 +01:00
Maze Winther 85f41756d1 feat: changelog page 2026-02-28 23:58:54 +01:00
Maze Winther fe964c4a4a cursor: design skill 2026-02-28 23:48:33 +01:00
Maze Winther 216e3e0c39 feat: introduce WebGL effects system and Blur effect
This implements the foundational architecture for video effects, starting with a multi-pass WebGL rendering pipeline and a customizable Gaussian Blur effect.

Key changes:
- WebGL Engine: Added `raw-loader` for `.glsl` shaders, multi-pass framebuffer rendering, and live offscreen canvas previews.
- Node Architecture: Replaced hardcoded background blur with `CompositeEffectNode` and added `EffectLayerNode` to apply effects to specific visual elements.
- Timeline & DND: Added a new `effect` track type. Upgraded drag-and-drop to support dropping effects directly onto the timeline. Consolidated track constants into a cleaner `TRACK_CONFIG`.
- UI/UX: Added an Effects tab in the assets panel with live previews. Added an Effect Properties panel with sliders and inputs for fine-tuning parameters.
- Data Model: Added `sourceDuration` to video and audio elements, and wrote a v8 storage migration to update existing projects to the new schema.
- Docs: Added `CHANGELOG.md` tracking v0.1.0 and v0.2.0, plus `docs/effects-renderer.md` to document the new WebGL pipeline.
2026-02-28 18:41:22 +01:00
a352eb1aab feat(projects): add right-click context menu to project cards (#711)
* feat(projects): add right-click context menu to project cards

- Add context menu with Rename, Duplicate, Info, Delete actions
- Works in both grid and list view
- Uses existing Radix UI ContextMenu components
- Mirrors actions from the existing ProjectMenu dropdown

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(projects): lift dialog state out of ProjectMenu and context menu

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Maze Winther <mazewinther@gmail.com>
2026-02-28 14:12:31 +01:00
Maze Winther a9e93471a7 review cursor command 2026-02-28 01:12:27 +01:00
Maze Winther 50885fb976 fix: update review checklist terminology for clarity 2026-02-28 00:26:09 +01:00
Maze Winther 6226ceb052 fix coderabbit issues 2026-02-27 20:34:33 +01:00
Maze Winther cb8f003478 another type error 2026-02-27 18:23:13 +01:00
Maze Winther 956f3d201b fix type error 2026-02-27 18:15:36 +01:00
Maze Winther 49426f19cd feat: implement keyframe animation system
Add keyframe support for transform, opacity, and volume properties.
Includes animation engine (interpolation, mutations, resolvers), timeline
markers with selection/snapping, properties panel toggles, keyframe-aware
renderer, and full undo/redo command support.

Also refactor element command constructors to object params, extract
timeline pixel math to pixel-utils.ts, and update cursor rules.
2026-02-27 16:33:57 +01:00
Maze Winther 9b94f89def roadmao 2026-02-25 04:51:50 +01:00
Maze Winther 07c2289168 feat: add properties for video/image/sticker elements 2026-02-25 04:22:00 +01:00
Maze Winther 987153266e refactor: move transform type to more relevant file 2026-02-25 04:14:21 +01:00
Dipanshu RawatandGitHub 6da55c7aee chore: improve accessibility and security (#707)
- Update roadmap description date from July 14, 2025 to February 2026
- Improve landing page hero image alt text for better a11y
- Add noreferrer to external links for security best practice
2026-02-25 03:27:27 +01:00
Maze Winther f96192289b docs: move sponsors near the top 2026-02-25 01:40:12 +01:00
Maze Winther f75b5d3fd2 fix a ton of issues + improvement 2026-02-25 01:37:56 +01:00
Maze Winther 4d77e3f2bb refactor: snapping system 2026-02-23 10:55:35 +01:00
Maze Winther ab64c48c92 feat: make playhead snap to elements 2026-02-23 10:55:06 +01:00
Maze Winther d352e58570 fix preview snapping threshold 2026-02-23 10:15:51 +01:00
Maze Winther baa5f33407 fix preview perf issues 2026-02-23 10:03:15 +01:00
Maze Winther dbfecbba59 docker + update readme 2026-02-23 09:38:38 +01:00
Maze Winther 86a3592c88 refactor: make export audio consistent to preview
fixes audio export issue with .mkv files
2026-02-23 09:28:54 +01:00
Maze Winther 744419469d feat: basic ripple editing 2026-02-23 07:09:26 +01:00
Maze Winther 1762dc077d persist snapping + ripple 2026-02-23 06:56:05 +01:00
Maze Winther a982304bb3 docs: actions 2026-02-23 06:50:53 +01:00
Maze Winther 5616d4e9f3 feat: save frame as image 2026-02-23 06:44:27 +01:00
Maze Winther 71970d5f5f fix: audio from video files not being exported 2026-02-23 06:20:11 +01:00
MazeandGitHub 93d1e3383c feat: major editor overhaul (assets, properties, timeline, fonts) (#709)
* feat: major editor overhaul (assets, properties, timeline, fonts)

Refactor editor core systems to standardize UI architecture and improve performance.

Assets & Properties:
- Replace monolithic property items with composable `Section` architecture.
- Add specialized sections for Transform, Blending, and Text.
- Implement `NumberField` with scrubbing and math evaluation.
- Add new ColorPicker with EyeDropper and multiple format support.
- Standardize asset panels using new `PanelView` layout.

Fonts & Stickers:
- Implement custom font atlas/sprite system for high-performance previews.
- Add virtualized FontPicker with search and favorites.
- Refactor stickers to use a provider-based architecture (icons, emoji, flags, shapes).
- Standardize sticker IDs to `provider:value` format.

Timeline & Interaction:
- Convert bookmarks to rich objects with notes, colors, and duration.
- Refactor drag-and-drop to use Command pattern (enabling proper undo/redo).
- Add Shift modifier to disable snapping during moves/resizes.
- Add new overlays for layout guides and text editing.

Renderer:
- Add support for multi-line text, custom line-height, and letter-spacing.
- Implement global composite operation (blend modes).
- Update sticker node to resolve dynamic provider IDs.

Infrastructure:
- Add storage migrations (v3->v6) for text weights, sticker IDs, and bookmarks.
- Update global styles and core UI components (Button, Input, Popover).

* add ts-nocheck directive to settings-legacy.tsx to suppress TypeScript errors

* fix: correct global composite operation assignment in TextNode to ensure proper blend mode handling

* deleted shadcn components with errors

* formatting

* fix linter issues

* migrate from next middleware to proxy

* add missing component back

* add breadcrumb back

* chore: add @radix-ui/react-primitive deps

* chore: more deps

* chore: add missing env vars to bun-ci

* next env
2026-02-23 03:24:02 +01:00
Maze Winther fca99d6126 refactor 2026-02-10 16:28:21 +01:00
Maze Winther 33a8705f0d fix selection not getting cleared after element deletion 2026-02-09 16:38:17 +01:00
Maze Winther fe10cee3eb fix: correct default text capitalization in text constants 2026-02-09 00:34:45 +01:00
Maze Winther 1d05f5ac56 fix linter 2026-02-08 22:23:12 +01:00
Maze Winther 395e415cc4 formatting 2026-02-08 22:22:23 +01:00
Maze Winther e50a817b97 add transform + few fixes
feat: transform
feat: batch command
fix: slider undo
fix: timeline zoom perf
refactor: make updateElement support multiple
2026-02-08 22:21:17 +01:00
Maze Winther 484ce47867 update review cursor command 2026-02-08 22:19:39 +01:00
Maze Winther 681f006b35 fix: fps not being displayed in some cases 2026-02-08 21:49:32 +01:00
Maze Winther 0282ef2223 scale timeline ruler labels to hours 2026-02-08 18:32:08 +01:00
Maze Winther dc0b57cd6a refactor: replace text-specific update command with generic 2026-02-08 18:01:45 +01:00
Maze Winther a218a52d2d persist sotring (projects) 2026-02-08 17:45:39 +01:00
Maze Winther 1c2cf80188 change default sorting from createdAt to updatedAt in projects store 2026-02-08 17:44:43 +01:00
Maze Winther cdd29eb1dc fix: resize-none 2026-02-08 17:06:16 +01:00
Maze Winther 34cfeacdbb fix: stop audio on timeline scrub 2026-02-08 17:02:49 +01:00
Maze Winther 6d55a34586 refactor: move isScrubbing state to playback manager 2026-02-08 17:01:46 +01:00
Maze Winther c6666fd385 feat: update text properties to use dynamic font size limits and scaling 2026-02-08 16:50:31 +01:00
Maze Winther 9b5fd6eb07 feat: ensure elements on main track starts at 0 2026-02-08 01:51:25 +01:00
Maze Winther 00cf7f6ab7 add todo 2026-02-08 01:23:13 +01:00
Maze Winther a82817a703 fix landing image 2026-02-08 01:17:29 +01:00
Maze Winther 39b6ea5613 type error 2026-02-07 23:57:19 +01:00
Maze Winther ae45ef8154 blog ui fixes 2026-02-07 23:14:31 +01:00
Maze Winther 49c4fcb229 refactor: remove AuthorList component 2026-02-07 22:37:19 +01:00
Maze Winther 464a6e83b9 fix rendering issue from v1 to current 2026-02-05 14:44:31 +01:00
Maze Winther d0b4b1a5fc fix migration dialog 2026-02-05 14:28:16 +01:00
Maze Winther 2257ca2e6f make storage versioning per-project + few other refactors 2026-02-05 12:53:56 +01:00
Maze Winther 490fc170c0 checkbox 2026-02-03 21:10:08 +01:00
Maze Winther feb8a07c16 remove comment 2026-02-03 14:00:43 +01:00
Maze Winther 7366cb27e9 fix v1 to v2 migration 2026-02-03 14:00:04 +01:00
Maze Winther 34daad9df7 comments 2026-02-03 08:48:32 +01:00
Maze Winther 9bc35363b9 remove hover state from export button 2026-02-03 08:33:42 +01:00
Maze Winther e84c0cfda6 comments + codebase index 2026-02-02 23:17:30 +01:00
Maze Winther 1dbc22b2f4 docs: update contribution guidelines and pull request template 2026-02-02 19:51:04 +01:00
Maze Winther ad794fa43b Merge branch 'timeline-zoom' 2026-02-02 19:46:51 +01:00
Maze Winther 9ca32e5e39 fix: click empty on track to move playhead 2026-02-02 19:44:27 +01:00
Maze Winther 5120e2efa6 feat: timeline zoom anchoring 2026-02-02 19:42:35 +01:00
Maze Winther 1aec9c5cf5 fix comment 2026-02-02 19:15:25 +01:00
Maze Winther e88a059222 increase zoom of buttons + refactor 2026-02-02 19:14:37 +01:00
Maze Winther c3f0d640ef feat: exponential zoom 2026-02-02 18:11:12 +01:00
Maze Winther 7d5914353b Merge branch 'staging' 2026-02-01 17:01:59 +01:00
Maze Winther ee7eaecc9f add comments 2026-02-01 16:44:07 +01:00
Maze Winther 8ea3db833f organize imports 2026-02-01 16:43:57 +01:00
Maze Winther 22ea6b8925 change rounding 2026-02-01 15:35:53 +01:00
Maze Winther dfccf905bb more test stuff 2026-02-01 13:31:44 +01:00
Maze Winther 3e4584edde fix 2026-02-01 13:19:51 +01:00
Maze Winther 4cd15fcf00 fix default panel sizes 2026-02-01 12:32:28 +01:00
Maze Winther 748856f4c4 fix onboarding text 2026-02-01 12:32:18 +01:00
Maze Winther 2759d7ed31 add tests for storage migrations 2026-02-01 12:15:37 +01:00
Maze Winther 03c2b16064 next-env.d.ts 2026-02-01 11:31:37 +01:00
Maze Winther e15ccefbe5 deps 2026-02-01 11:26:58 +01:00
Maze Winther 5e1e509c11 testing 4 2026-02-01 11:18:38 +01:00
Maze Winther bfcf20e108 testing 3 2026-02-01 11:16:18 +01:00
Maze Winther 4f64ff50cd testing 2 2026-02-01 11:09:55 +01:00
Maze Winther 58f23e8f2a testing 2026-02-01 11:06:20 +01:00
Maze Winther 33f4a54768 please 2026-02-01 10:59:05 +01:00
Maze Winther 1d55b903d7 fix remaining errors 2026-02-01 10:03:51 +01:00
Maze Winther af047ba4a7 more errors 2026-02-01 09:54:29 +01:00
Maze Winther ef3563745a fix errors 2026-02-01 02:10:24 +01:00
Maze Winther d06b7c3b0f update type definition for containerRef in ColorPickerProps to allow null 2026-02-01 01:44:48 +01:00
Maze Winther b7293dc552 fix type definition for ref in PanelBaseViewProps 2026-02-01 00:32:35 +01:00
Maze Winther 82d92934cb typescript error 2026-02-01 00:22:48 +01:00
Maze Winther b2a976ba82 fix type error (build) 2026-02-01 00:20:34 +01:00
Maze Winther 02d4f9c314 no idea 2026-02-01 00:20:10 +01:00
Maze Winther 1d31b7a586 fix import 2026-01-31 22:54:33 +01:00
Maze Winther e00ba706fe update deps 2026-01-31 22:24:22 +01:00
Maze Winther f820d9a894 refactor services structure 2026-01-31 22:19:11 +01:00
Maze Winther 7878c8bb7b add code review command 2026-01-31 22:18:39 +01:00
Nandni VermaandGitHub 1f618601c7 Clarify statement about CapCut features (#686) 2026-01-31 15:59:59 +01:00
Joep DuinandGitHub 59c189ca27 Github readme logo fix. (#699)
* Add files via upload

* Update logo image source in README.md

* Rename logo.png to apps/web/public/icons/github-logo.png

* Rename github-logo.png to github-logo.png

* Rename github-logo.png to github-logo.png

* Rename apps/web/public/logos/github-logo.png to apps/web/public/logos/opencut/white-black.png

* Rename apps/web/public/logos/opencut/white-black.png to apps/web/public/logos/opencut/1k/logo-white-black.png

* Update logo image source in README

* Update logo image path in README.md
2026-01-31 15:55:01 +01:00
MazeandGitHub 7bf0984698 codebase overhaul (#697) 2026-01-31 00:20:04 +01:00
337 changed files with 7096 additions and 37347 deletions
-106
View File
@@ -1,106 +0,0 @@
---
description: Ultracite Rules - AI-Ready Formatter and Linter
globs: "**/*.{ts,tsx,js,jsx}"
alwaysApply: true
---
# Project Context
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
## Key Principles
- Zero configuration required
- Subsecond performance
- Maximum type safety
- AI-friendly code generation
## Before Writing Code
1. Analyze existing patterns in the codebase
2. Consider edge cases and error scenarios
3. Follow the rules below strictly
4. Validate accessibility requirements
5. Avoid code duplication
## Rules
### Accessibility (a11y)
- Always include a `title` element for icons unless there's text beside the icon.
- Always include a `type` attribute for button elements.
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
### Code Complexity and Quality
- Don't use primitive type aliases or misleading types.
- Don't use the comma operator.
- Use for...of statements instead of Array.forEach.
- Don't initialize variables to undefined.
- Use .flatMap() instead of map().flat() when possible.
### React and JSX Best Practices
- Don't import `React` itself.
- Don't define React components inside other components.
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
- Don't insert comments as text nodes.
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
### Function Parameters and Props
- Always use destructured props objects instead of individual parameters in functions.
- Example: `function helloWorld({ prop }: { prop: string })` instead of `function helloWorld(param: string)`.
- This applies to all functions, not just React components.
### Correctness and Safety
- Don't assign a value to itself.
- Avoid unused imports and variables.
- Don't use await inside loops.
- Don't hardcode sensitive data like API keys and tokens.
- Don't use the TypeScript directive @ts-ignore.
- Make sure the `preconnect` attribute is used when using Google Fonts.
- Don't use the `delete` operator.
- Don't use `require()` in TypeScript/ES modules - use proper `import` statements.
### TypeScript Best Practices
- Don't use TypeScript enums.
- Use either `T[]` or `Array<T>` consistently.
- Don't use the `any` type.
### Style and Consistency
- Don't use global `eval()`.
- Use `String.slice()` instead of `String.substr()` and `String.substring()`.
- Don't use `else` blocks when the `if` block breaks early.
- Put default function parameters and optional function parameters last.
- Use `new` when throwing an error.
- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`.
### Next.js Specific Rules
- Don't use `<img>` elements in Next.js projects.
- Don't use `<head>` elements in Next.js projects.
## Example: Error Handling
```typescript
// ✅ Good: Comprehensive error handling
try {
const result = await fetchData();
return { success: true, data: result };
} catch (error) {
console.error("API call failed:", error);
return { success: false, error: error.message };
}
// ❌ Bad: Swallowing errors
try {
return await fetchData();
} catch (e) {
console.log(e);
}
```
-1
View File
@@ -1 +0,0 @@
!apps/web/.env.example
+89 -89
View File
@@ -1,89 +1,89 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at our discord server.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at our discord server.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
-178
View File
@@ -1,178 +0,0 @@
# Contributing to OpenCut
Thank you for your interest in contributing to OpenCut! This document provides guidelines and instructions for contributing.
## Getting Started
### Prerequisites
- [Node.js](https://nodejs.org/en/) (v18 or later)
- [Bun](https://bun.sh/docs/installation)
(for `npm` alternative)
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
### Setup
1. Fork the repository
2. Clone your fork locally
3. Navigate to the web app directory: `cd apps/web`
4. Copy `.env.example` to `.env.local`:
```bash
# Unix/Linux/Mac
cp .env.example .env.local
# Windows Command Prompt
copy .env.example .env.local
# Windows PowerShell
Copy-Item .env.example .env.local
```
5. Install dependencies: `bun install`
6. Start the development server: `bun run dev`
> **Note:** If you see an error like `Unsupported URL Type "workspace:*"` when running `npm install`, you have two options:
>
> 1. Upgrade to a recent npm version (v9 or later), which has full workspace protocol support.
> 2. Use an alternative package manager such as **bun** or **pnpm**.
## What to Focus On
**🎯 Good Areas to Contribute:**
- Timeline functionality and UI improvements
- Project management features
- Performance optimizations
- Bug fixes in existing functionality
- UI/UX improvements
- Documentation and testing
**⚠️ Areas to Avoid:**
- Preview panel enhancements (text fonts, stickers, effects)
- Export functionality improvements
- Preview rendering optimizations
**Why?** We're currently planning a major refactor of the preview system. The current preview renders DOM elements (HTML), but we're moving to a binary rendering approach similar to CapCut. This new system will ensure consistency between preview and export, and provide much better performance and quality.
The current HTML-based preview is essentially a prototype - the binary approach will be the "real deal." To avoid wasted effort, please focus on other areas of the application until this refactor is complete.
If you're unsure whether your idea falls into the preview category, feel free to ask us [directly in discord](https://discord.gg/zmR9N35cjK) or create a GitHub issue!
## Development Setup
### Local Development
1. Start the database and Redis services:
```bash
# From project root
docker-compose up -d
```
2. Navigate to the web app directory:
```bash
cd apps/web
```
3. Copy `.env.example` to `.env.local`:
```bash
# Unix/Linux/Mac
cp .env.example .env.local
# Windows Command Prompt
copy .env.example .env.local
# Windows PowerShell
Copy-Item .env.example .env.local
```
4. Configure required environment variables in `.env.local`:
**Required Variables:**
```bash
# Database (matches docker-compose.yaml)
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
# Generate a secure secret for Better Auth
BETTER_AUTH_SECRET="your-generated-secret-here"
NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
# Redis (matches docker-compose.yaml)
UPSTASH_REDIS_REST_URL="http://localhost:8079"
UPSTASH_REDIS_REST_TOKEN="example_token"
# Development
NODE_ENV="development"
```
**Generate BETTER_AUTH_SECRET:**
```bash
# Unix/Linux/Mac
openssl rand -base64 32
# Windows PowerShell (simple method)
[System.Web.Security.Membership]::GeneratePassword(32, 0)
# Cross-platform (using Node.js)
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Or use an online generator: https://generate-secret.vercel.app/32
```
5. Run database migrations: `bun run db:migrate`
6. Start the development server: `bun run dev`
## How to Contribute
### Reporting Bugs
- Use the bug report template
- Include steps to reproduce
- Provide screenshots if applicable
### Suggesting Features
- Use the feature request template
- Explain the use case
- Consider implementation details
### Code Contributions
1. Create a new branch: `git checkout -b feature/your-feature-name`
2. Make your changes
3. Navigate to the web app directory: `cd apps/web`
4. Run the linter: `bun run lint`
5. Format your code: `bunx biome format --write .`
6. Commit your changes with a descriptive message
7. Push to your fork and create a pull request
## Code Style
- We use Biome for code formatting and linting
- Run `bunx biome format --write .` from the `apps/web` directory to format code
- Run `bun run lint` from the `apps/web` directory to check for linting issues
- Follow the existing code patterns
## Pull Request Process
1. Fill out the pull request template completely
2. Link any related issues
3. Ensure CI passes
4. Request review from maintainers
5. Address any feedback
## Community
- Be respectful and inclusive
- Follow our Code of Conduct
- Help others in discussions and issues
Thank you for contributing!
+70 -70
View File
@@ -1,70 +1,70 @@
name: Bug report
description: Create a report to help us improve
title: '[BUG] '
labels: bug
body:
- type: input
id: Platform
attributes:
label: Platform
description: Please enter the platform on which you encountered the bug.
placeholder: e.g. Windows 11, Ubuntu 14.04
validations:
required: true
- type: input
id: Browser
attributes:
label: Browser
description: Please enter the browser on which you encountered the bug.
placeholder: e.g. Chrome 137, Firefox 137, Safari 17
validations:
required: true
- type: textarea
id: current-behavior
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: dropdown
id: recurrence-probability
attributes:
label: Recurrence Probability
description: How often does this bug occur?
options:
- Always
- Usually
- Sometimes
- Seldom
default: 0
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
name: Bug report
description: Create a report to help us improve
title: "[BUG] "
labels: bug
body:
- type: input
id: Platform
attributes:
label: Platform
description: Please enter the platform on which you encountered the bug.
placeholder: e.g. Windows 11, Ubuntu 14.04
validations:
required: true
- type: input
id: Browser
attributes:
label: Browser
description: Please enter the browser on which you encountered the bug.
placeholder: e.g. Chrome 137, Firefox 137, Safari 17
validations:
required: true
- type: textarea
id: current-behavior
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: false
- type: dropdown
id: recurrence-probability
attributes:
label: Recurrence Probability
description: How often does this bug occur?
options:
- Always
- Usually
- Sometimes
- Seldom
default: 0
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
+42 -42
View File
@@ -1,42 +1,42 @@
name: Feature request
description: Suggest an idea for OpenCut
title: '[FEATURE] '
labels: enhancement
body:
- type: markdown
attributes:
value: Please make sure that no duplicated issues has already been delivered.
- type: textarea
id: problem
attributes:
label: Problem
placeholder: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
placeholder: Describe the solution you'd like.
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
placeholder: Describe alternatives you've considered.
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
name: Feature request
description: Suggest an idea for OpenCut
title: "[FEATURE] "
labels: enhancement
body:
- type: markdown
attributes:
value: Please make sure that no duplicated issues has already been delivered.
- type: textarea
id: problem
attributes:
label: Problem
placeholder: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
placeholder: Describe the solution you'd like.
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
placeholder: Describe alternatives you've considered.
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
+28 -28
View File
@@ -1,28 +1,28 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
## Reporting a Vulnerability
We take security vulnerabilities seriously. If you discover a security vulnerability within OpenCut, please send an email to security@opencut.app. All security vulnerabilities will be promptly addressed.
Please do not report security vulnerabilities through public GitHub issues.
### What to include in your report
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Any suggested fixes
### Response timeline
- We will acknowledge receipt within 48 hours
- We will provide a detailed response within 5 business days
- We will keep you updated on our progress
Thank you for helping keep OpenCut secure!
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.x.x | :white_check_mark: |
## Reporting a Vulnerability
We take security vulnerabilities seriously. If you discover a security vulnerability within OpenCut, please send an email to security@opencut.app. All security vulnerabilities will be promptly addressed.
Please do not report security vulnerabilities through public GitHub issues.
### What to include in your report
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Any suggested fixes
### Response timeline
- We will acknowledge receipt within 48 hours
- We will provide a detailed response within 5 business days
- We will keep you updated on our progress
Thank you for helping keep OpenCut secure!
+27 -23
View File
@@ -1,23 +1,27 @@
# Getting Help
Thanks for using OpenCut! If you need help, here are your options:
## Documentation
- Check our [README](../README.md) for basic setup instructions
- Review the [Contributing Guidelines](CONTRIBUTING.md) for development setup
## Issues
- **Bug reports**: Use the bug report template
- **Feature requests**: Use the feature request template
- **Questions**: Use GitHub Discussions for general questions
## Community
- Join our discussions on GitHub
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md)
## Response Times
- Issues are typically triaged within 2-3 business days
- Feature requests may take longer to evaluate
- Security issues are handled with priority
We appreciate your patience and contributions to making OpenCut better!
# Getting Help
Thanks for using OpenCut! If you need help, here are your options:
## Documentation
- Check our [README](../README.md) for basic setup instructions
- Review the [Contributing Guidelines](CONTRIBUTING.md) for development setup
## Issues
- **Bug reports**: Use the bug report template
- **Feature requests**: Use the feature request template
- **Questions**: Use GitHub Discussions for general questions
## Community
- Join our discussions on GitHub
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md)
## Response Times
- Issues are typically triaged within 2-3 business days
- Feature requests may take longer to evaluate
- Security issues are handled with priority
We appreciate your patience and contributions to making OpenCut better!
+344 -331
View File
@@ -1,331 +1,344 @@
---
applyTo: "**/*.{ts,tsx,js,jsx}"
---
# Project Context
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
## Key Principles
- Zero configuration required
- Subsecond performance
- Maximum type safety
- AI-friendly code generation
## Before Writing Code
1. Analyze existing patterns in the codebase
2. Consider edge cases and error scenarios
3. Follow the rules below strictly
4. Validate accessibility requirements
## Rules
### Accessibility (a11y)
- Don't use `accessKey` attribute on any HTML element.
- Don't set `aria-hidden="true"` on focusable elements.
- Don't add ARIA roles, states, and properties to elements that don't support them.
- Don't use distracting elements like `<marquee>` or `<blink>`.
- Only use the `scope` prop on `<th>` elements.
- Don't assign non-interactive ARIA roles to interactive HTML elements.
- Make sure label elements have text content and are associated with an input.
- Don't assign interactive ARIA roles to non-interactive HTML elements.
- Don't assign `tabIndex` to non-interactive HTML elements.
- Don't use positive integers for `tabIndex` property.
- Don't include "image", "picture", or "photo" in img alt prop.
- Don't use explicit role property that's the same as the implicit/default role.
- Make static elements with click handlers use a valid role attribute.
- Always include a `title` element for SVG elements.
- Give all elements requiring alt text meaningful information for screen readers.
- Make sure anchors have content that's accessible to screen readers.
- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`.
- Include all required ARIA attributes for elements with ARIA roles.
- Make sure ARIA properties are valid for the element's supported roles.
- Always include a `type` attribute for button elements.
- Make elements with interactive roles and handlers focusable.
- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`).
- Always include a `lang` attribute on the html element.
- Always include a `title` attribute for iframe elements.
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
- Include caption tracks for audio and video elements.
- Use semantic elements instead of role attributes in JSX.
- Make sure all anchors are valid and navigable.
- Ensure all ARIA properties (`aria-*`) are valid.
- Use valid, non-abstract ARIA roles for elements with ARIA roles.
- Use valid ARIA state and property values.
- Use valid values for the `autocomplete` attribute on input elements.
- Use correct ISO language/country codes for the `lang` attribute.
### Code Complexity and Quality
- Don't use consecutive spaces in regular expression literals.
- Don't use the `arguments` object.
- Don't use primitive type aliases or misleading types.
- Don't use the comma operator.
- Don't use empty type parameters in type aliases and interfaces.
- Don't write functions that exceed a given Cognitive Complexity score.
- Don't nest describe() blocks too deeply in test files.
- Don't use unnecessary boolean casts.
- Don't use unnecessary callbacks with flatMap.
- Use for...of statements instead of Array.forEach.
- Don't create classes that only have static members (like a static namespace).
- Don't use this and super in static contexts.
- Don't use unnecessary catch clauses.
- Don't use unnecessary constructors.
- Don't use unnecessary continue statements.
- Don't export empty modules that don't change anything.
- Don't use unnecessary escape sequences in regular expression literals.
- Don't use unnecessary fragments.
- Don't use unnecessary labels.
- Don't use unnecessary nested block statements.
- Don't rename imports, exports, and destructured assignments to the same name.
- Don't use unnecessary string or template literal concatenation.
- Don't use String.raw in template literals when there are no escape sequences.
- Don't use useless case statements in switch statements.
- Don't use ternary operators when simpler alternatives exist.
- Don't use useless `this` aliasing.
- Don't use any or unknown as type constraints.
- Don't initialize variables to undefined.
- Don't use the void operators (they're not familiar).
- Use arrow functions instead of function expressions.
- Use Date.now() to get milliseconds since the Unix Epoch.
- Use .flatMap() instead of map().flat() when possible.
- Use literal property access instead of computed property access.
- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
- Use concise optional chaining instead of chained logical expressions.
- Use regular expression literals instead of the RegExp constructor when possible.
- Don't use number literal object member names that aren't base 10 or use underscore separators.
- Remove redundant terms from logical expressions.
- Use while loops instead of for loops when you don't need initializer and update expressions.
- Don't pass children as props.
- Don't reassign const variables.
- Don't use constant expressions in conditions.
- Don't use `Math.min` and `Math.max` to clamp values when the result is constant.
- Don't return a value from a constructor.
- Don't use empty character classes in regular expression literals.
- Don't use empty destructuring patterns.
- Don't call global object properties as functions.
- Don't declare functions and vars that are accessible outside their block.
- Make sure builtins are correctly instantiated.
- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors.
- Don't use variables and function parameters before they're declared.
- Don't use 8 and 9 escape sequences in string literals.
- Don't use literal numbers that lose precision.
### React and JSX Best Practices
- Don't use the return value of React.render.
- Make sure all dependencies are correctly specified in React hooks.
- Make sure all React hooks are called from the top level of component functions.
- Don't forget key props in iterators and collection literals.
- Don't destructure props inside JSX components in Solid projects.
- Don't define React components inside other components.
- Don't use event handlers on non-interactive elements.
- Don't assign to React component props.
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
- Don't use dangerous JSX props.
- Don't use Array index in keys.
- Don't insert comments as text nodes.
- Don't assign JSX properties multiple times.
- Don't add extra closing tags for components without children.
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
- Watch out for possible "wrong" semicolons inside JSX elements.
### Correctness and Safety
- Don't assign a value to itself.
- Don't return a value from a setter.
- Don't compare expressions that modify string case with non-compliant values.
- Don't use lexical declarations in switch clauses.
- Don't use variables that haven't been declared in the document.
- Don't write unreachable code.
- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass.
- Don't use control flow statements in finally blocks.
- Don't use optional chaining where undefined values aren't allowed.
- Don't have unused function parameters.
- Don't have unused imports.
- Don't have unused labels.
- Don't have unused private class members.
- Don't have unused variables.
- Make sure void (self-closing) elements don't have children.
- Don't return a value from a function with the return type 'void'
- Use isNaN() when checking for NaN.
- Make sure "for" loop update clauses move the counter in the right direction.
- Make sure typeof expressions are compared to valid values.
- Make sure generator functions contain yield.
- Don't use await inside loops.
- Don't use bitwise operators.
- Don't use expressions where the operation doesn't change the value.
- Make sure Promise-like statements are handled appropriately.
- Don't use __dirname and __filename in the global scope.
- Prevent import cycles.
- Don't use configured elements.
- Don't hardcode sensitive data like API keys and tokens.
- Don't let variable declarations shadow variables from outer scopes.
- Don't use the TypeScript directive @ts-ignore.
- Prevent duplicate polyfills from Polyfill.io.
- Don't use useless backreferences in regular expressions that always match empty strings.
- Don't use unnecessary escapes in string literals.
- Don't use useless undefined.
- Make sure getters and setters for the same property are next to each other in class and object definitions.
- Make sure object literals are declared consistently (defaults to explicit definitions).
- Use static Response methods instead of new Response() constructor when possible.
- Make sure switch-case statements are exhaustive.
- Make sure the `preconnect` attribute is used when using Google Fonts.
- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item.
- Make sure iterable callbacks return consistent values.
- Use `with { type: "json" }` for JSON module imports.
- Use numeric separators in numeric literals.
- Use object spread instead of `Object.assign()` when constructing new objects.
- Always use the radix argument when using `parseInt()`.
- Make sure JSDoc comment lines start with a single asterisk, except for the first one.
- Include a description parameter for `Symbol()`.
- Don't use spread (`...`) syntax on accumulators.
- Don't use the `delete` operator.
- Don't access namespace imports dynamically.
- Don't use namespace imports.
- Declare regex literals at the top level.
- Don't use `target="_blank"` without `rel="noopener"`.
### TypeScript Best Practices
- Don't use TypeScript enums.
- Don't export imported variables.
- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
- Don't use TypeScript namespaces.
- Don't use non-null assertions with the `!` postfix operator.
- Don't use parameter properties in class constructors.
- Don't use user-defined types.
- Use `as const` instead of literal types and type annotations.
- Use either `T[]` or `Array<T>` consistently.
- Initialize each enum member value explicitly.
- Use `export type` for types.
- Use `import type` for types.
- Make sure all enum members are literal values.
- Don't use TypeScript const enum.
- Don't declare empty interfaces.
- Don't let variables evolve into any type through reassignments.
- Don't use the any type.
- Don't misuse the non-null assertion operator (!) in TypeScript files.
- Don't use implicit any type on variable declarations.
- Don't merge interfaces and classes unsafely.
- Don't use overload signatures that aren't next to each other.
- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
### Style and Consistency
- Don't use global `eval()`.
- Don't use callbacks in asynchronous tests and hooks.
- Don't use negation in `if` statements that have `else` clauses.
- Don't use nested ternary expressions.
- Don't reassign function parameters.
- This rule lets you specify global variable names you don't want to use in your application.
- Don't use specified modules when loaded by import or require.
- Don't use constants whose value is the upper-case version of their name.
- Use `String.slice()` instead of `String.substr()` and `String.substring()`.
- Don't use template literals if you don't need interpolation or special-character handling.
- Don't use `else` blocks when the `if` block breaks early.
- Don't use yoda expressions.
- Don't use Array constructors.
- Use `at()` instead of integer index access.
- Follow curly brace conventions.
- Use `else if` instead of nested `if` statements in `else` clauses.
- Use single `if` statements instead of nested `if` clauses.
- Use `new` for all builtins except `String`, `Number`, and `Boolean`.
- Use consistent accessibility modifiers on class properties and methods.
- Use `const` declarations for variables that are only assigned once.
- Put default function parameters and optional function parameters last.
- Include a `default` clause in switch statements.
- Use the `**` operator instead of `Math.pow`.
- Use `for-of` loops when you need the index to extract an item from the iterated array.
- Use `node:assert/strict` over `node:assert`.
- Use the `node:` protocol for Node.js builtin modules.
- Use Number properties instead of global ones.
- Use assignment operator shorthand where possible.
- Use function types instead of object types with call signatures.
- Use template literals over string concatenation.
- Use `new` when throwing an error.
- Don't throw non-Error values.
- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`.
- Use standard constants instead of approximated literals.
- Don't assign values in expressions.
- Don't use async functions as Promise executors.
- Don't reassign exceptions in catch clauses.
- Don't reassign class members.
- Don't compare against -0.
- Don't use labeled statements that aren't loops.
- Don't use void type outside of generic or return types.
- Don't use console.
- Don't use control characters and escape sequences that match control characters in regular expression literals.
- Don't use debugger.
- Don't assign directly to document.cookie.
- Use `===` and `!==`.
- Don't use duplicate case labels.
- Don't use duplicate class members.
- Don't use duplicate conditions in if-else-if chains.
- Don't use two keys with the same name inside objects.
- Don't use duplicate function parameter names.
- Don't have duplicate hooks in describe blocks.
- Don't use empty block statements and static blocks.
- Don't let switch clauses fall through.
- Don't reassign function declarations.
- Don't allow assignments to native objects and read-only global variables.
- Use Number.isFinite instead of global isFinite.
- Use Number.isNaN instead of global isNaN.
- Don't assign to imported bindings.
- Don't use irregular whitespace characters.
- Don't use labels that share a name with a variable.
- Don't use characters made with multiple code points in character class syntax.
- Make sure to use new and constructor properly.
- Don't use shorthand assign when the variable appears on both sides.
- Don't use octal escape sequences in string literals.
- Don't use Object.prototype builtins directly.
- Don't redeclare variables, functions, classes, and types in the same scope.
- Don't have redundant "use strict".
- Don't compare things where both sides are exactly the same.
- Don't let identifiers shadow restricted names.
- Don't use sparse arrays (arrays with holes).
- Don't use template literal placeholder syntax in regular strings.
- Don't use the then property.
- Don't use unsafe negation.
- Don't use var.
- Don't use with statements in non-strict contexts.
- Make sure async functions actually use await.
- Make sure default clauses in switch statements come last.
- Make sure to pass a message value when creating a built-in error.
- Make sure get methods always return a value.
- Use a recommended display strategy with Google Fonts.
- Make sure for-in loops include an if statement.
- Use Array.isArray() instead of instanceof Array.
- Make sure to use the digits argument with Number#toFixed().
- Make sure to use the "use strict" directive in script files.
### Next.js Specific Rules
- Don't use `<img>` elements in Next.js projects.
- Don't use `<head>` elements in Next.js projects.
- Don't import next/document outside of pages/_document.jsx in Next.js projects.
- Don't use the next/head module in pages/_document.js on Next.js projects.
### Testing Best Practices
- Don't use export or module.exports in test files.
- Don't use focused tests.
- Make sure the assertion function, like expect, is placed inside an it() function call.
- Don't use disabled tests.
## Common Tasks
- `npx ultracite init` - Initialize Ultracite in your project
- `npx ultracite format` - Format and fix code automatically
- `npx ultracite lint` - Check for issues without fixing
## Example: Error Handling
```typescript
// ✅ Good: Comprehensive error handling
try {
const result = await fetchData();
return { success: true, data: result };
} catch (error) {
console.error('API call failed:', error);
return { success: false, error: error.message };
}
// ❌ Bad: Swallowing errors
try {
return await fetchData();
} catch (e) {
console.log(e);
}
```
---
applyTo: "**/*.{ts,tsx,js,jsx}"
---
# Project Context
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
## Key Principles
- Zero configuration required
- Subsecond performance
- Maximum type safety
- AI-friendly code generation
## Before Writing Code
1. Analyze existing patterns in the codebase
2. Consider edge cases and error scenarios
3. Follow the rules below strictly
4. Validate accessibility requirements
## Rules
### Accessibility (a11y)
- Don't use `accessKey` attribute on any HTML element.
- Don't set `aria-hidden="true"` on focusable elements.
- Don't add ARIA roles, states, and properties to elements that don't support them.
- Don't use distracting elements like `<marquee>` or `<blink>`.
- Only use the `scope` prop on `<th>` elements.
- Don't assign non-interactive ARIA roles to interactive HTML elements.
- Make sure label elements have text content and are associated with an input.
- Don't assign interactive ARIA roles to non-interactive HTML elements.
- Don't assign `tabIndex` to non-interactive HTML elements.
- Don't use positive integers for `tabIndex` property.
- Don't include "image", "picture", or "photo" in img alt prop.
- Don't use explicit role property that's the same as the implicit/default role.
- Make static elements with click handlers use a valid role attribute.
- Always include a `title` element for SVG elements.
- Give all elements requiring alt text meaningful information for screen readers.
- Make sure anchors have content that's accessible to screen readers.
- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`.
- Include all required ARIA attributes for elements with ARIA roles.
- Make sure ARIA properties are valid for the element's supported roles.
- Always include a `type` attribute for button elements.
- Make elements with interactive roles and handlers focusable.
- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`).
- Always include a `lang` attribute on the html element.
- Always include a `title` attribute for iframe elements.
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
- Include caption tracks for audio and video elements.
- Use semantic elements instead of role attributes in JSX.
- Make sure all anchors are valid and navigable.
- Ensure all ARIA properties (`aria-*`) are valid.
- Use valid, non-abstract ARIA roles for elements with ARIA roles.
- Use valid ARIA state and property values.
- Use valid values for the `autocomplete` attribute on input elements.
- Use correct ISO language/country codes for the `lang` attribute.
### Code Complexity and Quality
- Don't use consecutive spaces in regular expression literals.
- Don't use the `arguments` object.
- Don't use primitive type aliases or misleading types.
- Don't use the comma operator.
- Don't use empty type parameters in type aliases and interfaces.
- Don't write functions that exceed a given Cognitive Complexity score.
- Don't nest describe() blocks too deeply in test files.
- Don't use unnecessary boolean casts.
- Don't use unnecessary callbacks with flatMap.
- Use for...of statements instead of Array.forEach.
- Don't create classes that only have static members (like a static namespace).
- Don't use this and super in static contexts.
- Don't use unnecessary catch clauses.
- Don't use unnecessary constructors.
- Don't use unnecessary continue statements.
- Don't export empty modules that don't change anything.
- Don't use unnecessary escape sequences in regular expression literals.
- Don't use unnecessary fragments.
- Don't use unnecessary labels.
- Don't use unnecessary nested block statements.
- Don't rename imports, exports, and destructured assignments to the same name.
- Don't use unnecessary string or template literal concatenation.
- Don't use String.raw in template literals when there are no escape sequences.
- Don't use useless case statements in switch statements.
- Don't use ternary operators when simpler alternatives exist.
- Don't use useless `this` aliasing.
- Don't use any or unknown as type constraints.
- Don't initialize variables to undefined.
- Don't use the void operators (they're not familiar).
- Use arrow functions instead of function expressions.
- Use Date.now() to get milliseconds since the Unix Epoch.
- Use .flatMap() instead of map().flat() when possible.
- Use literal property access instead of computed property access.
- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
- Use concise optional chaining instead of chained logical expressions.
- Use regular expression literals instead of the RegExp constructor when possible.
- Don't use number literal object member names that aren't base 10 or use underscore separators.
- Remove redundant terms from logical expressions.
- Use while loops instead of for loops when you don't need initializer and update expressions.
- Don't pass children as props.
- Don't reassign const variables.
- Don't use constant expressions in conditions.
- Don't use `Math.min` and `Math.max` to clamp values when the result is constant.
- Don't return a value from a constructor.
- Don't use empty character classes in regular expression literals.
- Don't use empty destructuring patterns.
- Don't call global object properties as functions.
- Don't declare functions and vars that are accessible outside their block.
- Make sure builtins are correctly instantiated.
- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors.
- Don't use variables and function parameters before they're declared.
- Don't use 8 and 9 escape sequences in string literals.
- Don't use literal numbers that lose precision.
### React and JSX Best Practices
- Don't use the return value of React.render.
- Make sure all dependencies are correctly specified in React hooks.
- Make sure all React hooks are called from the top level of component functions.
- Don't forget key props in iterators and collection literals.
- Don't destructure props inside JSX components in Solid projects.
- Don't define React components inside other components.
- Don't use event handlers on non-interactive elements.
- Don't assign to React component props.
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
- Don't use dangerous JSX props.
- Don't use Array index in keys.
- Don't insert comments as text nodes.
- Don't assign JSX properties multiple times.
- Don't add extra closing tags for components without children.
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
- Watch out for possible "wrong" semicolons inside JSX elements.
### Correctness and Safety
- Don't assign a value to itself.
- Don't return a value from a setter.
- Don't compare expressions that modify string case with non-compliant values.
- Don't use lexical declarations in switch clauses.
- Don't use variables that haven't been declared in the document.
- Don't write unreachable code.
- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass.
- Don't use control flow statements in finally blocks.
- Don't use optional chaining where undefined values aren't allowed.
- Don't have unused function parameters.
- Don't have unused imports.
- Don't have unused labels.
- Don't have unused private class members.
- Don't have unused variables.
- Make sure void (self-closing) elements don't have children.
- Don't return a value from a function with the return type 'void'
- Use isNaN() when checking for NaN.
- Make sure "for" loop update clauses move the counter in the right direction.
- Make sure typeof expressions are compared to valid values.
- Make sure generator functions contain yield.
- Don't use await inside loops.
- Don't use bitwise operators.
- Don't use expressions where the operation doesn't change the value.
- Make sure Promise-like statements are handled appropriately.
- Don't use **dirname and **filename in the global scope.
- Prevent import cycles.
- Don't use configured elements.
- Don't hardcode sensitive data like API keys and tokens.
- Don't let variable declarations shadow variables from outer scopes.
- Don't use the TypeScript directive @ts-ignore.
- Prevent duplicate polyfills from Polyfill.io.
- Don't use useless backreferences in regular expressions that always match empty strings.
- Don't use unnecessary escapes in string literals.
- Don't use useless undefined.
- Make sure getters and setters for the same property are next to each other in class and object definitions.
- Make sure object literals are declared consistently (defaults to explicit definitions).
- Use static Response methods instead of new Response() constructor when possible.
- Make sure switch-case statements are exhaustive.
- Make sure the `preconnect` attribute is used when using Google Fonts.
- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item.
- Make sure iterable callbacks return consistent values.
- Use `with { type: "json" }` for JSON module imports.
- Use numeric separators in numeric literals.
- Use object spread instead of `Object.assign()` when constructing new objects.
- Always use the radix argument when using `parseInt()`.
- Make sure JSDoc comment lines start with a single asterisk, except for the first one.
- Include a description parameter for `Symbol()`.
- Don't use spread (`...`) syntax on accumulators.
- Don't use the `delete` operator.
- Don't access namespace imports dynamically.
- Don't use namespace imports.
- Declare regex literals at the top level.
- Don't use `target="_blank"` without `rel="noopener"`.
### TypeScript Best Practices
- Don't use TypeScript enums.
- Don't export imported variables.
- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
- Don't use TypeScript namespaces.
- Don't use non-null assertions with the `!` postfix operator.
- Don't use parameter properties in class constructors.
- Don't use user-defined types.
- Use `as const` instead of literal types and type annotations.
- Use either `T[]` or `Array<T>` consistently.
- Initialize each enum member value explicitly.
- Use `export type` for types.
- Use `import type` for types.
- Make sure all enum members are literal values.
- Don't use TypeScript const enum.
- Don't declare empty interfaces.
- Don't let variables evolve into any type through reassignments.
- Don't use the any type.
- Don't misuse the non-null assertion operator (!) in TypeScript files.
- Don't use implicit any type on variable declarations.
- Don't merge interfaces and classes unsafely.
- Don't use overload signatures that aren't next to each other.
- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
### Style and Consistency
- Don't use global `eval()`.
- Don't use callbacks in asynchronous tests and hooks.
- Don't use negation in `if` statements that have `else` clauses.
- Don't use nested ternary expressions.
- Don't reassign function parameters.
- This rule lets you specify global variable names you don't want to use in your application.
- Don't use specified modules when loaded by import or require.
- Don't use constants whose value is the upper-case version of their name.
- Use `String.slice()` instead of `String.substr()` and `String.substring()`.
- Don't use template literals if you don't need interpolation or special-character handling.
- Don't use `else` blocks when the `if` block breaks early.
- Don't use yoda expressions.
- Don't use Array constructors.
- Use `at()` instead of integer index access.
- Follow curly brace conventions.
- Use `else if` instead of nested `if` statements in `else` clauses.
- Use single `if` statements instead of nested `if` clauses.
- Use `new` for all builtins except `String`, `Number`, and `Boolean`.
- Use consistent accessibility modifiers on class properties and methods.
- Use `const` declarations for variables that are only assigned once.
- Put default function parameters and optional function parameters last.
- Include a `default` clause in switch statements.
- Use the `**` operator instead of `Math.pow`.
- Use `for-of` loops when you need the index to extract an item from the iterated array.
- Use `node:assert/strict` over `node:assert`.
- Use the `node:` protocol for Node.js builtin modules.
- Use Number properties instead of global ones.
- Use assignment operator shorthand where possible.
- Use function types instead of object types with call signatures.
- Use template literals over string concatenation.
- Use `new` when throwing an error.
- Don't throw non-Error values.
- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`.
- Use standard constants instead of approximated literals.
- Don't assign values in expressions.
- Don't use async functions as Promise executors.
- Don't reassign exceptions in catch clauses.
- Don't reassign class members.
- Don't compare against -0.
- Don't use labeled statements that aren't loops.
- Don't use void type outside of generic or return types.
- Don't use console.
- Don't use control characters and escape sequences that match control characters in regular expression literals.
- Don't use debugger.
- Don't assign directly to document.cookie.
- Use `===` and `!==`.
- Don't use duplicate case labels.
- Don't use duplicate class members.
- Don't use duplicate conditions in if-else-if chains.
- Don't use two keys with the same name inside objects.
- Don't use duplicate function parameter names.
- Don't have duplicate hooks in describe blocks.
- Don't use empty block statements and static blocks.
- Don't let switch clauses fall through.
- Don't reassign function declarations.
- Don't allow assignments to native objects and read-only global variables.
- Use Number.isFinite instead of global isFinite.
- Use Number.isNaN instead of global isNaN.
- Don't assign to imported bindings.
- Don't use irregular whitespace characters.
- Don't use labels that share a name with a variable.
- Don't use characters made with multiple code points in character class syntax.
- Make sure to use new and constructor properly.
- Don't use shorthand assign when the variable appears on both sides.
- Don't use octal escape sequences in string literals.
- Don't use Object.prototype builtins directly.
- Don't redeclare variables, functions, classes, and types in the same scope.
- Don't have redundant "use strict".
- Don't compare things where both sides are exactly the same.
- Don't let identifiers shadow restricted names.
- Don't use sparse arrays (arrays with holes).
- Don't use template literal placeholder syntax in regular strings.
- Don't use the then property.
- Don't use unsafe negation.
- Don't use var.
- Don't use with statements in non-strict contexts.
- Make sure async functions actually use await.
- Make sure default clauses in switch statements come last.
- Make sure to pass a message value when creating a built-in error.
- Make sure get methods always return a value.
- Use a recommended display strategy with Google Fonts.
- Make sure for-in loops include an if statement.
- Use Array.isArray() instead of instanceof Array.
- Make sure to use the digits argument with Number#toFixed().
- Make sure to use the "use strict" directive in script files.
### Next.js Specific Rules
- Don't use `<img>` elements in Next.js projects.
- Don't use `<head>` elements in Next.js projects.
- Don't import next/document outside of pages/\_document.jsx in Next.js projects.
- Don't use the next/head module in pages/\_document.js on Next.js projects.
### Testing Best Practices
- Don't use export or module.exports in test files.
- Don't use focused tests.
- Make sure the assertion function, like expect, is placed inside an it() function call.
- Don't use disabled tests.
## Common Tasks
- `npx ultracite init` - Initialize Ultracite in your project
- `npx ultracite format` - Format and fix code automatically
- `npx ultracite lint` - Check for issues without fixing
## Example: Error Handling
```typescript
// ✅ Good: Comprehensive error handling
try {
const result = await fetchData();
return { success: true, data: result };
} catch (error) {
console.error("API call failed:", error);
return { success: false, error: error.message };
}
// ❌ Bad: Swallowing errors
try {
return await fetchData();
} catch (e) {
console.log(e);
}
```
+11 -49
View File
@@ -1,49 +1,11 @@
## Description
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Performance improvement
- [ ] Code refactoring
- [ ] Tests
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] Test A
- [ ] Test B
**Test Configuration**:
* Node version:
* Browser (if applicable):
* Operating System:
## Screenshots (if applicable)
Add screenshots to help explain your changes.
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have added screenshots if ui has been changed
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
## Additional context
Add any other context about the pull request here.
⚠️ READ BEFORE SUBMITTING ⚠️
We are not currently accepting PRs except for critical bugs.
If this is a bug fix:
- [ ] I've opened an issue first
- [ ] This was approved by a maintainer
If this is a feature:
This PR will be closed. Please open an issue to discuss first.
+37 -57
View File
@@ -1,57 +1,37 @@
name: Bun CI
concurrency:
group: bun-ci-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths-ignore:
- "*.md"
pull_request:
branches: [main]
paths-ignore:
- "*.md"
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
DATABASE_URL: "postgresql://opencut:opencutthegoat@localhost:5432/opencut"
BETTER_AUTH_SECRET: "supersecret"
NEXT_PUBLIC_BETTER_AUTH_URL: "http://localhost:3000"
UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url"
UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
with:
bun-version: 1.2.18
- name: Cache Bun modules
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-1.2.18-${{ hashFiles('apps/web/bun.lock') }}
- name: Install dependencies
working-directory: apps/web
run: bun install
- name: Build
working-directory: apps/web
run: bun run build
- name: Run tests
working-directory: apps/web
run: echo "No tests implemented yet"
continue-on-error: true
name: Bun CI
concurrency:
group: bun-ci-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths-ignore:
- "*.md"
pull_request:
branches: [main]
paths-ignore:
- "*.md"
jobs:
ci:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup toolchain
uses: moonrepo/setup-toolchain@v0
with:
auto-install: true
auto-setup: true
- name: Run CI
run: moon ci
+9 -33
View File
@@ -1,34 +1,10 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules/
dist/
target/
.env
.env.*
!.env.example
docs/
# dependencies
/apps/web/node_modules
# next.js
/apps/web/.next/
/apps/web/out/
# debug
/apps/web/npm-debug.log*
# env files (can opt-in for committing if needed)
/apps/web/.env*
!/apps/web/.env.example
# typescript
/apps/web/next-env.d.ts
# asdf version management
.tool-versions
node_modules
.cursorignore
.turbo
*.env
# cursor
bun.lockb
apps/transcription/__pycache__
apps/transcription/env
apps/bg-remover/env
# moon runtime cache
.moon/cache/
-1
View File
@@ -1 +0,0 @@
npx ultracite format
+12
View File
@@ -0,0 +1,12 @@
$schema: 'https://moonrepo.dev/schemas/toolchains.json'
# javascript must be enabled alongside bun
javascript: {}
bun:
version: '1.3.11'
# Auto-install deps when lockfile/manifest changes
installDependencies: true
# Version is inherited from .prototools (versionFromPrototools)
rust: {}
+6
View File
@@ -0,0 +1,6 @@
$schema: 'https://moonrepo.dev/schemas/workspace.json'
# Discover all projects under apps/ automatically.
# When shared crates land under crates/, add a 'crates/*' glob here.
projects:
- 'apps/*'
-2
View File
@@ -1,2 +0,0 @@
install-strategy="nested"
node-linker=isolated
+5
View File
@@ -0,0 +1,5 @@
# proto pins tool versions workspace-wide.
# Every developer and CI machine gets the exact same versions automatically.
moon = "2.3.3"
bun = "1.3.11"
rust = "1.97.0"
+7
View File
@@ -0,0 +1,7 @@
*.json
*.svg
*.xml
*.lock
**/dist/**
**/build/**
**/node_modules/**
+20 -14
View File
@@ -1,14 +1,20 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": {
"editor.defaultFormatter": "biomejs.biome"
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"emmet.showExpandedAbbreviation": "never",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"emmet.showExpandedAbbreviation": "never",
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
-168
View File
@@ -1,168 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
OpenCut is a free, open-source video editor built with Next.js, focusing on privacy (no server processing), multi-track timeline editing, and real-time preview. The project is a monorepo using Turborepo with multiple apps including a web application, desktop app (Tauri), background remover tools, and transcription services.
## Essential Commands
**Development:**
```bash
# Root level development
bun dev # Start all apps in development mode
bun build # Build all apps
bun lint # Lint all code using Ultracite
bun format # Format all code using Ultracite
# Web app specific (from apps/web/)
cd apps/web
bun run dev # Start Next.js development server with Turbopack
bun run build # Build for production
bun run lint # Run Biome linting
bun run lint:fix # Fix linting issues automatically
bun run format # Format code with Biome
# Database operations (from apps/web/)
bun run db:generate # Generate Drizzle migrations
bun run db:migrate # Run migrations
bun run db:push:local # Push schema to local development database
bun run db:push:prod # Push schema to production database
```
**Testing:**
- No unified test commands are currently configured
- Individual apps may have their own test setups
## Architecture & Key Components
### State Management
The application uses **Zustand** for state management with separate stores for different concerns:
- **editor-store.ts**: Canvas presets, layout guides, app initialization
- **timeline-store.ts**: Timeline tracks, elements, playback state
- **media-store.ts**: Media files and asset management
- **playback-store.ts**: Video playback controls and timing
- **project-store.ts**: Project-level data and persistence
- **panel-store.ts**: UI panel visibility and layout
- **keybindings-store.ts**: Keyboard shortcut management
- **sounds-store.ts**: Audio effects and sound management
- **stickers-store.ts**: Sticker/graphics management
### Storage System
**Multi-layer storage approach:**
- **IndexedDB**: Projects, saved sounds, and structured data
- **OPFS (Origin Private File System)**: Large media files for better performance
- **Storage Service** (`lib/storage/`): Abstraction layer managing both storage types
### Editor Architecture
**Core editor components:**
- **Timeline Canvas**: Custom canvas-based timeline with tracks and elements
- **Preview Panel**: Real-time video preview (currently DOM-based, planned binary refactor)
- **Media Panel**: Asset management with drag-and-drop support
- **Properties Panel**: Context-sensitive element properties
### Media Processing
- **FFmpeg Integration**: Client-side video processing using @ffmpeg/ffmpeg
- **Background Removal**: Python-based tools with multiple AI models (U2Net, SAM, Gemini)
- **Transcription**: Separate service for audio-to-text conversion
## Development Focus Areas
**✅ Recommended contribution areas:**
- Timeline functionality and UI improvements
- Project management features
- Performance optimizations
- Bug fixes in existing functionality
- UI/UX improvements outside preview panel
- Documentation and testing
**⚠️ Areas to avoid (pending refactor):**
- Preview panel enhancements (fonts, stickers, effects)
- Export functionality improvements
- Preview rendering optimizations
**Reason:** The preview system is planned for a major refactor from DOM-based rendering to binary rendering for consistency with export and better performance.
## Code Quality Standards
**Linting & Formatting:**
- Uses **Biome** for JavaScript/TypeScript linting and formatting
- Extends **Ultracite** configuration for strict type safety and AI-friendly code
- Comprehensive accessibility (a11y) rules enforced
- Zero configuration approach with subsecond performance
**Key coding standards from Ultracite:**
- Strict TypeScript with no `any` types
- No React imports (uses automatic JSX runtime)
- Comprehensive accessibility requirements
- Use `for...of` instead of `Array.forEach`
- No TypeScript enums, use const objects
- Always include error handling with try-catch
## Environment Setup
**Required environment variables (apps/web/.env.local):**
```bash
# Database
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
# Authentication
BETTER_AUTH_SECRET="your-generated-secret-here"
BETTER_AUTH_URL="http://localhost:3000"
# Redis
UPSTASH_REDIS_REST_URL="http://localhost:8079"
UPSTASH_REDIS_REST_TOKEN="example_token"
# Content Management
MARBLE_WORKSPACE_KEY="workspace-key"
NEXT_PUBLIC_MARBLE_API_URL="https://api.marblecms.com"
```
**Docker services:**
```bash
# Start local database and Redis
docker-compose up -d
```
## Project Structure
**Monorepo layout:**
- `apps/web/` - Main Next.js application
- `apps/desktop/` - Tauri desktop application
- `apps/bg-remover/` - Python background removal tools
- `apps/transcription/` - Audio transcription service
- `packages/` - Shared packages (auth, database)
**Web app structure:**
- `src/components/` - React components organized by feature
- `src/stores/` - Zustand state management
- `src/hooks/` - Custom React hooks
- `src/lib/` - Utility functions and services
- `src/types/` - TypeScript type definitions
- `src/app/` - Next.js app router pages and API routes
## Common Patterns
**Error handling:**
```typescript
try {
const result = await processData();
return { success: true, data: result };
} catch (error) {
console.error('Operation failed:', error);
return { success: false, error: error.message };
}
```
**Store usage:**
```typescript
const { tracks, addTrack, updateTrack } = useTimelineStore();
```
**Media processing:**
```typescript
import { processVideo } from '@/lib/ffmpeg-utils';
const processedVideo = await processVideo(inputFile, options);
```
+13
View File
@@ -0,0 +1,13 @@
[workspace]
resolver = "3"
members = [
'apps/desktop',
]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"
[workspace.dependencies]
gpui = "0.2.2"
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2025 OpenCut
Copyright 2026 OpenCut
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+34 -147
View File
@@ -1,177 +1,64 @@
<table width="100%">
<tr>
<td align="left" width="120">
<img src="apps/web/public/logo.png" alt="OpenCut Logo" width="100" />
<img src="https://assets.opencut.app/branding/symbol.svg" alt="OpenCut Logo" width="100" />
</td>
<td align="right">
<h1>OpenCut</span></h1>
<h3 style="margin-top: -10px;">A free, open-source video editor for web, desktop, and mobile.</h3>
<h1>OpenCut</h1>
<h3 style="margin-top: -10px;">A free and open source video editor for web, desktop, and mobile.</h3>
</td>
</tr>
</table>
## Why?
[![Discord](https://img.shields.io/discord/1386309140057690133?label=Discord&logo=discord&logoColor=fff&color=5865F2&style=flat)](https://discord.gg/zmR9N35cjK)
[![X](https://img.shields.io/badge/follow-%40opencutapp-000?logo=x&logoColor=fff&style=flat)](https://x.com/opencutapp)
[![License: MIT](https://img.shields.io/badge/license-MIT-green?style=flat)](LICENSE)
- **Privacy**: Your videos stay on your device
- **Free features**: Every basic feature of CapCut is paywalled now
- **Simple**: People want editors that are easy to use - CapCut proved that
## Status
## Features
**OpenCut is being rewritten from the ground up.** What's coming:
- Timeline-based editing
- Multi-track support
- Real-time preview
- No watermarks or subscriptions
- Analytics provided by [Databuddy](https://www.databuddy.cc?utm_source=opencut), 100% Anonymized & Non-invasive.
- Blog powered by [Marble](https://marblecms.com?utm_source=opencut), Headless CMS.
- An Editor API
- First-class third party plugins (made possible by a plugin-first architecture)
- Desktop, mobile, and browser from one codebase (Rust core)
- MCP server (for AI agents)
- Headless mode (automation, batch rendering)
- A scripting tab directly in the editor
## Project Structure
You can still find the previous version at [opencut-app/opencut-classic](https://github.com/opencut-app/opencut-classic), which is the one to reach for today. [opencut.app](https://opencut.app) still runs the classic version. The rewrite will live at [new.opencut.app](https://new.opencut.app) until it's ready to take over.
- `apps/web/` – Main Next.js web application
- `src/components/` – UI and editor components
- `src/hooks/` – Custom React hooks
- `src/lib/` – Utility and API logic
- `src/stores/` – State management (Zustand, etc.)
- `src/types/` – TypeScript types
## Development
## Getting Started
Install [proto](https://moonrepo.dev/proto) if you haven't already:
### Prerequisites
```sh
bash <(curl -fsSL https://moonrepo.dev/install/proto.sh)
```
Before you begin, ensure you have the following installed on your system:
From the repo root:
- [Node.js](https://nodejs.org/en/) (v18 or later)
- [Bun](https://bun.sh/docs/installation)
(for `npm` alternative)
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
```sh
proto use # installs the tools pinned in .prototools
```
> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to run the frontend or want to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
### Setup
1. Fork the repository
2. Clone your fork locally
3. Navigate to the web app directory: `cd apps/web`
4. Copy `.env.example` to `.env.local`:
```bash
# Unix/Linux/Mac
cp .env.example .env.local
# Windows Command Prompt
copy .env.example .env.local
# Windows PowerShell
Copy-Item .env.example .env.local
```
5. Install dependencies: `bun install`
6. Start the development server: `bun dev`
## Development Setup
### Local Development
1. Start the database and Redis services:
```bash
# From project root
docker-compose up -d
```
2. Navigate to the web app directory:
```bash
cd apps/web
```
3. Copy `.env.example` to `.env.local`:
```bash
# Unix/Linux/Mac
cp .env.example .env.local
# Windows Command Prompt
copy .env.example .env.local
# Windows PowerShell
Copy-Item .env.example .env.local
```
4. Configure required environment variables in `.env.local`:
**Required Variables:**
```bash
# Database (matches docker-compose.yaml)
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
# Generate a secure secret for Better Auth
BETTER_AUTH_SECRET="your-generated-secret-here"
BETTER_AUTH_URL="http://localhost:3000"
# Redis (matches docker-compose.yaml)
UPSTASH_REDIS_REST_URL="http://localhost:8079"
UPSTASH_REDIS_REST_TOKEN="example_token"
# Marble Blog
MARBLE_WORKSPACE_KEY=cm6ytuq9x0000i803v0isidst # example organization key
NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
# Development
NODE_ENV="development"
```
**Generate BETTER_AUTH_SECRET:**
```bash
# Unix/Linux/Mac
openssl rand -base64 32
# Windows PowerShell (simple method)
[System.Web.Security.Membership]::GeneratePassword(32, 0)
# Cross-platform (using Node.js)
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Or use an online generator: https://generate-secret.vercel.app/32
```
5. Run database migrations: `bun run db:migrate` from (inside apps/web)
6. Start the development server: `bun run dev` from (inside apps/web)
The application will be available at [http://localhost:3000](http://localhost:3000).
```sh
moon run web:dev # localhost:5173
moon run api:dev # localhost:8787
moon run desktop:dev # see apps/desktop/README.md
```
## Contributing
We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
**🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel.
**⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach.
See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance.
**Quick start for contributors:**
- Fork the repo and clone locally
- Follow the setup instructions in CONTRIBUTING.md
- Create a feature branch and submit a PR
We're not set up to take outside contributions yet while the architecture is being designed. If you want to follow along, ask questions, or just hang out, [join the Discord](https://discord.gg/zmR9N35cjK) or [open an issue](https://github.com/opencut-app/opencut/issues).
## Sponsors
<a href="https://fal.ai">
<img alt="Powered by fal.ai" src="https://img.shields.io/badge/Powered%20by-fal.ai-000000?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCAxMEwxMy4wOSAxNS43NEwxMiAyMkwxMC45MSAxNS43NEw0IDEwTDEwLjkxIDguMjZMMTIgMloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" />
</a>
OpenCut is supported by companies that believe in open source creator tools.
---
- [**fal.ai**](https://fal.ai?utm_source=github-opencut&utm_campaign=oss): Generative image, video, and audio models all in one place.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FOpenCut-app%2FOpenCut&project-name=opencut&repository-name=opencut)
Want your logo here? Reach out at [sponsor@opencut.app](mailto:sponsor@opencut.app).
## License
[MIT LICENSE](LICENSE)
---
![Star History Chart](https://api.star-history.com/svg?repos=opencut-app/opencut&type=Date)
[MIT](LICENSE)
+24
View File
@@ -0,0 +1,24 @@
$schema: 'https://moonrepo.dev/schemas/project.json'
language: 'typescript'
layer: 'application'
tasks:
dev:
command: 'bun run dev'
options:
runInCI: false # dev server: skipped in CI, never cached
build:
command: 'bun run build'
inputs:
- 'src/**/*'
- 'wrangler.jsonc'
- 'package.json'
outputs:
- 'dist'
deploy:
command: 'bun run deploy'
deps:
- '~:build'
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@opencut/api",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"build": "wrangler deploy --dry-run"
},
"dependencies": {
"elysia": "latest"
},
"devDependencies": {
"@cloudflare/workers-types": "latest",
"wrangler": "latest"
}
}
+15
View File
@@ -0,0 +1,15 @@
import { Elysia, t } from "elysia";
import { CloudflareAdapter } from "elysia/adapter/cloudflare-worker";
export default new Elysia({ adapter: CloudflareAdapter })
.get("/", () => ({ status: "ok" }))
.get("/health", () => ({ healthy: true, timestamp: new Date().toISOString() }))
.post(
"/echo",
({ body }) => body,
{
body: t.Object({ message: t.String() }),
}
)
// .compile() is required, it triggers AoT compilation at startup
.compile();
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "opencut-api",
"main": "src/index.ts",
// Minimum date required for CloudflareAdapter AoT support
"compatibility_date": "2025-06-01"
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "opencut-desktop"
description = "OpenCut desktop app, built with GPUI"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
name = "opencut-desktop"
path = "src/main.rs"
[dependencies]
gpui = { workspace = true }
+24
View File
@@ -0,0 +1,24 @@
# OpenCut Desktop
Built with [GPUI](https://www.gpui.rs).
> [!WARNING]
> Very early. Right now this is just a window that opens.
## Running
Rust is pinned in `.prototools` at the repo root (`proto use` installs it).
```sh
moon run desktop:dev # cargo run
moon run desktop:check # cargo check
moon run desktop:build # cargo build --release
```
The first build compiles GPUI from source and takes a while. The root `Cargo.lock` is committed.
## Platform requirements
- **macOS**: Xcode command line tools (Metal renderer).
- **Windows**: no extra dependencies (Win32 + DirectWrite).
- **Linux**: renders via Vulkan (Blade), windows via Wayland or X11 (both enabled by default). System packages (Debian/Ubuntu names): `libvulkan1` + working Vulkan drivers, `libwayland-dev`, `libx11-xcb-dev`, `libxkbcommon-x11-dev`, `libfontconfig-dev`, plus a C toolchain and `cmake`.
+28
View File
@@ -0,0 +1,28 @@
$schema: 'https://moonrepo.dev/schemas/project.json'
language: 'rust'
layer: 'application'
tasks:
dev:
command: 'cargo run'
options:
runInCI: false
check:
command: 'cargo check'
inputs:
- 'src/**/*'
- 'Cargo.toml'
- '/Cargo.toml'
- '/Cargo.lock'
build:
command: 'cargo build --release'
inputs:
- 'src/**/*'
- 'Cargo.toml'
- '/Cargo.toml'
- '/Cargo.lock'
outputs:
- '/target/release/opencut-desktop'
+51
View File
@@ -0,0 +1,51 @@
use gpui::{
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString,
TitlebarOptions, Window, WindowBounds, WindowOptions,
};
struct Root {
status: SharedString,
}
impl Render for Root {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_2()
.size_full()
.justify_center()
.items_center()
.bg(rgb(0x111111))
.text_color(rgb(0xffffff))
.child(div().text_xl().child("OpenCut"))
.child(
div()
.text_sm()
.text_color(rgb(0x888888))
.child(self.status.clone()),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(960.), px(600.)), cx);
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(SharedString::from("OpenCut")),
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| {
cx.new(|_| Root {
status: "desktop shell scaffold".into(),
})
},
)
.expect("failed to open the main window");
});
}
-86
View File
@@ -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!
-5
View File
@@ -1,5 +0,0 @@
modal
openai-whisper
boto3
pydantic
cryptography
-143
View File
@@ -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")
+19
View File
@@ -0,0 +1,19 @@
{
"projectName": "web",
"mode": "file-router",
"typescript": true,
"packageManager": "npm",
"includeExamples": false,
"tailwind": true,
"addOnOptions": {},
"envVarValues": {},
"git": false,
"install": true,
"routerOnly": false,
"version": 1,
"framework": "react",
"chosenAddOns": [
"cloudflare",
"shadcn"
]
}
-33
View File
@@ -1,33 +0,0 @@
# Environment Variables Example
# Copy this file to .env.local and update the values as needed
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
# Better Auth
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your-secret-key-here
# Development Environment
NODE_ENV=development
# Redis
UPSTASH_REDIS_REST_URL=http://localhost:8079
UPSTASH_REDIS_REST_TOKEN=example_token
# Marble Blog
MARBLE_WORKSPACE_KEY=cm6ytuq9x0000i803v0isidst # example organization key
NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
# Freesound (generate at https://freesound.org/apiv2/apply/)
FREESOUND_CLIENT_ID=...
FREESOUND_API_KEY=...
# 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
# Modal transcription endpoint (from modal deploy transcription.py)
MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run
+13 -2
View File
@@ -1,2 +1,13 @@
# Turborepo
.turbo
node_modules
.DS_Store
dist
dist-ssr
*.local
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig*
todos.json
+11
View File
@@ -0,0 +1,11 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}
-65
View File
@@ -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"]
+13 -9
View File
@@ -1,21 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"style": "base-mira",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"css": "src/styles.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "hugeicons",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
"components": "#/components",
"utils": "#/lib/utils",
"ui": "#/components/ui",
"lib": "#/lib",
"hooks": "#/hooks"
},
"iconLibrary": "lucide"
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
-19
View File
@@ -1,19 +0,0 @@
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
// Load the right env file based on environment
if (process.env.NODE_ENV === "production") {
dotenv.config({ path: ".env.production" });
} else {
dotenv.config({ path: ".env.local" });
}
export default {
schema: "../../packages/db/src/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
out: "./migrations",
strict: process.env.NODE_ENV === "production",
} satisfies Config;
@@ -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;
@@ -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;
@@ -1 +0,0 @@
ALTER TABLE "users" ALTER COLUMN "email_verified" SET DEFAULT false;
-10
View File
@@ -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;
-307
View File
@@ -1,307 +0,0 @@
{
"id": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e",
"prevId": "00000000-0000-0000-0000-000000000000",
"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
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
-344
View File
@@ -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": {}
}
}
-345
View File
@@ -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": {}
}
}
-365
View File
@@ -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": {}
}
}
-34
View File
@@ -1,34 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"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",
"breakpoints": true
}
]
}
+32
View File
@@ -0,0 +1,32 @@
$schema: 'https://moonrepo.dev/schemas/project.json'
language: 'typescript'
layer: 'application'
tasks:
dev:
command: 'bun run dev'
options:
runInCI: false # dev server: skipped in CI, never cached
build:
command: 'bun run build'
inputs:
- 'src/**/*'
- 'public/**/*'
- 'vite.config.ts'
- 'tsconfig.json'
- 'package.json'
outputs:
- 'dist'
test:
command: 'bun run test'
inputs:
- 'src/**/*'
- 'tsconfig.json'
deploy:
command: 'bun run deploy'
deps:
- '~:build'
-49
View File
@@ -1,49 +0,0 @@
import type { NextConfig } from "next";
import { withBotId } from "botid/next/config";
const nextConfig: NextConfig = {
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},
reactStrictMode: true,
productionBrowserSourceMaps: true,
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "plus.unsplash.com",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
{
protocol: "https",
hostname: "images.marblecms.com",
},
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
},
{
protocol: "https",
hostname: "api.iconify.design",
},
{
protocol: "https",
hostname: "api.simplesvg.com",
},
{
protocol: "https",
hostname: "api.unisvg.com",
},
],
},
};
export default withBotId(nextConfig);
+62 -78
View File
@@ -1,89 +1,73 @@
{
"name": "opencut",
"version": "0.1.0",
"name": "@opencut/web",
"private": true,
"packageManager": "bun@1.2.18",
"type": "module",
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "biome check src/",
"lint:fix": "biome check src/ --write",
"format": "biome format src/ --write",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push:local": "cross-env NODE_ENV=development drizzle-kit push",
"db:push:prod": "cross-env NODE_ENV=production drizzle-kit push"
"dev": "vite dev --port 5173",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"deploy": "bun run build && wrangler deploy"
},
"dependencies": {
"@ffmpeg/core": "^0.12.10",
"@ffmpeg/ffmpeg": "^0.12.15",
"@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",
"@vercel/analytics": "^1.4.1",
"aws4fetch": "^1.0.20",
"better-auth": "^1.2.7",
"botid": "^1.4.2",
"@base-ui/react": "^1.4.1",
"@cloudflare/vite-plugin": "^1.26.0",
"@fontsource-variable/inter": "^5.2.8",
"@hookform/resolvers": "^5.2.2",
"@hugeicons/core-free-icons": "^4.1.2",
"@hugeicons/react": "^1.1.6",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest",
"@tanstack/react-router": "latest",
"@tanstack/react-router-devtools": "latest",
"@tanstack/react-router-ssr-query": "latest",
"@tanstack/react-start": "latest",
"@tanstack/router-plugin": "^1.132.0",
"class-variance-authority": "^0.7.1",
"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",
"feed": "^5.1.0",
"framer-motion": "^11.13.1",
"input-otp": "^1.4.1",
"lucide-react": "^0.468.0",
"motion": "^12.18.1",
"nanoid": "^5.1.5",
"next": "^15.5.7",
"next-themes": "^0.4.4",
"pg": "^8.16.2",
"radix-ui": "^1.4.2",
"react": "^18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.54.0",
"react-icons": "^5.4.0",
"react-markdown": "^10.1.0",
"react-phone-number-input": "^3.4.11",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.14.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-parse": "^9.0.1",
"rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0",
"rehype-stringify": "^10.0.1",
"sonner": "^1.7.1",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"vaul": "^1.1.1",
"zod": "^3.25.67",
"zustand": "^5.0.2"
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^1.14.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-day-picker": "^10.0.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.75.0",
"react-resizable-panels": "^4.11.0",
"recharts": "3.8.0",
"shadcn": "^4.7.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"vaul": "^1.1.2",
"zod": "^4.4.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@types/bun": "latest",
"@types/node": "^24.2.1",
"@types/pg": "^8.15.4",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.4",
"postcss": "^8",
"tailwindcss": "^4.1.11",
"tsx": "^4.7.1",
"typescript": "^5.8.3"
"@tanstack/devtools-vite": "latest",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^6.0.1",
"jsdom": "^28.1.0",
"typescript": "^6.0.2",
"vite": "^8.0.0",
"vitest": "^4.1.5",
"wrangler": "^4.70.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"lightningcss"
]
}
}
}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/icons/ms-icon-70x70.png"/>
<square150x150logo src="/icons/ms-icon-150x150.png"/>
<square310x310logo src="/icons/ms-icon-310x310.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 195 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
<svg width="459" height="77" viewBox="0 0 459 77" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="1.15625" width="458" height="75" rx="15.5" fill="#101010"/>
<rect x="0.5" y="1.15625" width="458" height="75" rx="15.5" stroke="#FFCC00"/>
<rect x="0.5" y="1.15625" width="31" height="75" rx="15.5" fill="#2A2A2A"/>
<rect x="0.5" y="1.15625" width="31" height="75" rx="15.5" stroke="#FFCC00"/>
<rect x="13" y="20.6562" width="6" height="36" rx="3" fill="#FFCC00"/>
<rect x="427.5" y="1.15625" width="31" height="75" rx="15.5" fill="#2A2A2A"/>
<rect x="427.5" y="1.15625" width="31" height="75" rx="15.5" stroke="#FFCC00"/>
<rect x="440" y="20.6562" width="6" height="36" rx="3" fill="#FFCC00"/>
</svg>

Before

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

-10
View File
@@ -1,10 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_10_2)">
<path d="M32 9.37305V22.627L22.627 32H9.37305L0 22.627V9.37305L9.37305 0H22.627L32 9.37305ZM8 8V24H24V8H8Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_10_2">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 362 B

+16 -35
View File
@@ -1,44 +1,25 @@
{
"name": "OpenCut",
"description": "A simple but powerful video editor that gets the job done. In your browser.",
"display": "standalone",
"start_url": "/",
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "/icons/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "/icons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/icons/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "/icons/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "/icons/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "/icons/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
]
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
-146
View File
@@ -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 (
<div className="flex h-screen items-center justify-center relative">
<Button
variant="text"
onClick={() => router.back()}
className="absolute top-6 left-6"
>
<ArrowLeft className="h-5 w-5" /> Back
</Button>
<Card className="w-[400px] shadow-lg border-0">
<CardHeader className="text-center pb-4">
<CardTitle className="text-2xl font-semibold">Welcome back</CardTitle>
<CardDescription className="text-base">
Sign in to your account to continue
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<Suspense
fallback={
<div className="text-center">
<Loader2 className="animate-spin" />
</div>
}
>
<div className="flex flex-col space-y-6">
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleGoogleLogin}
variant="outline"
size="lg"
disabled={isAnyLoading}
>
{isGoogleLoading ? (
<Loader2 className="animate-spin" />
) : (
<GoogleIcon />
)}{" "}
Continue with Google
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<Button
onClick={handleLogin}
disabled={isAnyLoading || !email || !password}
className="w-full h-11"
size="lg"
>
{isEmailLoading ? (
<Loader2 className="animate-spin" />
) : (
"Sign in"
)}
</Button>
</div>
</div>
<div className="mt-6 text-center text-sm">
Don't have an account?{" "}
<Link
href="/signup"
className="font-medium text-primary underline-offset-4 hover:underline"
>
Sign up
</Link>
</div>
</Suspense>
</CardContent>
</Card>
</div>
);
};
export default memo(LoginPage);
-162
View File
@@ -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 (
<div className="flex h-screen items-center justify-center relative">
<Button
variant="text"
onClick={() => router.back()}
className="absolute top-6 left-6"
>
<ArrowLeft className="h-5 w-5" /> Back
</Button>
<Card className="w-[400px] shadow-lg border-0">
<CardHeader className="text-center pb-4">
<CardTitle className="text-2xl font-semibold">
Create your account
</CardTitle>
<CardDescription className="text-base">
Get started with your free account today
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<Suspense
fallback={
<div className="text-center">
<Loader2 className="animate-spin" />
</div>
}
>
<div className="flex flex-col space-y-6">
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleGoogleSignUp}
variant="outline"
size="lg"
disabled={isAnyLoading}
>
{isGoogleLoading ? (
<Loader2 className="animate-spin" />
) : (
<GoogleIcon />
)}{" "}
Continue with Google
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
type="text"
placeholder="John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Create a strong password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<Button
onClick={handleSignUp}
disabled={isAnyLoading || !name || !email || !password}
className="w-full h-11"
size="lg"
>
{isEmailLoading ? (
<Loader2 className="animate-spin" />
) : (
"Create account"
)}
</Button>
</div>
</div>
<div className="mt-6 text-center text-sm">
Already have an account?{" "}
<Link
href="/login"
className="font-medium text-primary underline-offset-4 hover:underline"
>
Sign in
</Link>
</div>
</Suspense>
</CardContent>
</Card>
</div>
);
};
export default memo(SignUpPage);
-24
View File
@@ -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 (
<div className="h-dvh flex">
<div className="bg-panel p-6 w-[20rem]">
<InputWithBack isExpanded={isExpanded} setIsExpanded={setIsExpanded} />
</div>
<div className="p-6 flex items-end justify-end">
<p
onClick={() => setIsExpanded(!isExpanded)}
className="cursor-pointer hover:opacity-75 transition-opacity"
>
{isExpanded ? "Collapse" : "Expand"}
</p>
</div>
</div>
);
}
@@ -1,4 +0,0 @@
import { auth } from "@opencut/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
@@ -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 }
);
}
}
-5
View File
@@ -1,5 +0,0 @@
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
return new Response("OK", { status: 200 });
}
-265
View File
@@ -1,265 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { env } from "@/env";
import { baseRateLimit } 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),
});
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(),
});
const freesoundResponseSchema = z.object({
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(),
});
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(),
});
export async function GET(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 { searchParams } = new URL(request.url);
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 (!validationResult.success) {
return NextResponse.json(
{
error: "Invalid parameters",
details: validationResult.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const {
q: query,
type,
page,
page_size: pageSize,
sort,
min_rating,
commercial_only,
} = validationResult.data;
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 baseUrl = "https://freesound.org/apiv2/search/text/";
// Use score sorting for search queries, downloads for top sounds
const sortParam = query
? sort === "score"
? "score"
: `${sort}_desc`
: `${sort}_desc`;
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",
});
// 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 *]`);
// 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")'
);
}
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 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 }
);
}
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 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 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 }
);
}
return NextResponse.json(responseValidation.data);
} catch (error) {
console.error("Error searching sounds:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
-189
View File
@@ -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 }
);
}
}
@@ -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 }
);
}
}
-144
View File
@@ -1,144 +0,0 @@
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 Image from "next/image";
import { notFound } from "next/navigation";
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
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),
],
},
};
}
export async function generateStaticParams() {
const data = await getPosts();
if (!data || !data.posts.length) return [];
return data.posts.map((post) => ({
slug: post.slug,
}));
}
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 (
<div className="min-h-screen bg-background">
<Header />
<main className="relative">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
</div>
<div className="relative container max-w-3xl mx-auto px-4 py-16">
<div className="text-center mb-6">
{data.post.coverImage && (
<div className="relative aspect-video rounded-lg overflow-hidden mb-6">
<Image
src={data.post.coverImage}
alt={data.post.title}
loading="eager"
fill
className="object-cover rounded-lg"
/>
</div>
)}
<div className="flex items-center justify-center mb-6">
<time dateTime={data.post.publishedAt.toString()}>
{formattedDate}
</time>
</div>
<h1 className="text-5xl md:text-4xl font-bold tracking-tight mb-6">
{data.post.title}
</h1>
<div className="flex items-center justify-center gap-2">
{data.post.authors[0] && (
<>
<Image
src={data.post.authors[0].image}
alt={data.post.authors[0].name}
width={36}
height={36}
loading="eager"
className="aspect-square shrink-0 size-8 rounded-full"
/>
<p className="text-muted-foreground">
{data.post.authors[0].name}
</p>
</>
)}
</div>
</div>
<Separator />
<section className="mt-14">
<Prose html={html} />
</section>
</div>
</main>
</div>
);
}
export default Page;
-98
View File
@@ -1,98 +0,0 @@
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 Link from "next/link";
import { getPosts } from "@/lib/blog-query";
import Image from "next/image";
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",
},
};
export default async function BlogPage() {
const data = await getPosts();
if (!data || !data.posts) return <div>No posts yet</div>;
return (
<div className="min-h-screen bg-background">
<Header />
<main className="relative">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
</div>
<div className="relative container max-w-3xl mx-auto px-4 py-16">
<div className="text-center mb-20">
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
Blog
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
Read the latest news and updates about OpenCut, the free and
open-source video editor.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{data.posts.map((post) => (
<Link key={post.id} href={`/blog/${post.slug}`}>
<Card className="h-full hover:shadow-lg transition-shadow overflow-hidden">
{post.coverImage && (
<div className="relative aspect-video">
<Image
src={post.coverImage}
alt={post.title}
fill
className="object-cover rounded-xl"
/>
</div>
)}
<CardContent className="p-6">
{post.authors && post.authors.length > 0 && (
<div className="flex items-center gap-2 mb-4">
{post.authors.map((author, index) => (
<div
key={author.id}
className="flex items-center gap-2"
>
<Avatar className="w-6 h-6">
<AvatarImage
src={author.image}
alt={author.name}
/>
<AvatarFallback className="text-xs">
{author.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="text-sm text-muted-foreground">
{author.name}
</span>
{index < post.authors.length - 1 && (
<span className="text-muted-foreground">•</span>
)}
</div>
))}
</div>
)}
<h2 className="text-xl font-semibold mb-2">{post.title}</h2>
<p className="text-muted-foreground">{post.description}</p>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
</main>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More