Compare commits

...
Author SHA1 Message Date
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
MazeandGitHub 0173db9944 Update next dependency version to 15.5.7 2025-12-07 22:25:35 +01:00
MazeandGitHub 154353ee66 Update next dependency version to 15.5.7 2025-12-07 22:24:42 +01:00
MazeandGitHub bee7aba960 Update README.md 2025-09-30 03:42:01 +02:00
MazeandGitHub a346bdf497 docs: fal sponsor 2025-09-12 01:04:48 +02:00
Maze Winther f483cb98cf Merge branch 'staging' 2025-09-03 00:11:02 +02:00
Maze Winther 88304c8a90 fix: ts error 2025-09-03 00:10:56 +02:00
Maze Winther 60debe2ce5 Merge branch 'staging' 2025-09-03 00:02:07 +02:00
Maze Winther 167e2b83dc feat: add support for deleting/splitting multiple elements instead of single 2025-09-02 23:07:11 +02:00
Maze Winther c5c32a1e34 style: make plus button bigger 2025-09-01 23:20:01 +02:00
Maze Winther f7ef8d7f38 move logic to zustand store 2025-09-01 23:17:48 +02:00
Maze Winther 42cf851d63 disable back animation 2025-09-01 23:02:57 +02:00
Maze Winther c5480d307c refactor: make stickers use panel base view 2025-09-01 23:01:15 +02:00
Maze Winther 3d67b065f5 refactor: remove success toast 2025-09-01 22:19:55 +02:00
Maze Winther 9c00da37b9 actually fix border 2025-09-01 22:17:12 +02:00
Maze Winther d8f446580c feat: remove default background scene and add scene management 2025-09-01 22:15:32 +02:00
Maze Winther b84e699e9c fix: border issue 2025-09-01 17:27:39 +02:00
Maze Winther af1964cf11 fix: scenes now load properly 2025-09-01 17:18:00 +02:00
Maze Winther e2bd8d1e09 fix: don't pre-fetch sounds 2025-09-01 16:31:03 +02:00
Maze Winther 971f9d35d7 fix: normal case instead of title case 2025-09-01 16:20:13 +02:00
Maze Winther dd02e31380 refactor: reduce code duplication 2025-09-01 16:19:14 +02:00
Maze Winther 350b55081c refactor(core): switch to scene-based architecture 2025-09-01 16:17:06 +02:00
Maze Winther 2b700b7474 feat: scenes button to timeline toolbar 2025-08-31 23:25:26 +02:00
Maze Winther e43d79e4c5 style: fix timeline toolbar border to extend full width 2025-08-31 22:39:20 +02:00
Maze Winther 45d0caf979 style: spacing 2025-08-31 21:55:36 +02:00
Maze Winther 6e32d4aed5 refactor: move timeline toolbar to separate file 2025-08-31 21:54:42 +02:00
Maze Winther 8eabbeae7c more changes 2025-08-31 21:51:31 +02:00
Maze Winther 2c5d5dc234 bunch of small improvements 2025-08-31 19:57:50 +02:00
Maze Winther 46a74b30ba update roadmap to check off export 2025-08-31 14:43:25 +02:00
Maze Winther 73a156c002 gradient backgrounds now work 2025-08-31 14:35:24 +02:00
Maze Winther 29fdd8568d refactor: improve types, fix background blur, some other stuff 2025-08-31 14:10:59 +02:00
Maze Winther 3db7666467 more styling 2025-08-31 13:53:13 +02:00
Maze Winther ae50aee4b6 style: spacing 2025-08-31 13:48:29 +02:00
Maze Winther e7d75ce1f8 fix: add fallback 2025-08-31 13:43:52 +02:00
Maze Winther d747fb458d feat: make timeline renderer 15x faster 2025-08-31 13:40:41 +02:00
Maze Winther e222f15d1d feat: implement timeline caching with cache indicator 2025-08-31 13:26:53 +02:00
Maze Winther 21dac9f09e fix space key to play 2025-08-31 00:58:33 +02:00
Maze Winther 37d4014808 Merge branch 'staging' 2025-08-31 00:20:14 +02:00
Maze Winther 5abddb0a22 idk 2025-08-31 00:04:41 +02:00
Maze Winther 598afab08b style: fix spacing 2025-08-31 00:03:55 +02:00
Maze Winther 2afd4ec6fb feat: switching theme across entire site 2025-08-31 00:01:38 +02:00
Maze Winther e98e40028e typescript: fix any types 2025-08-30 20:45:47 +02:00
Maze Winther 454a308644 Merge branch 'staging' 2025-08-29 00:05:57 +02:00
Maze Winther 886cf81bd0 feat: timeline auto-scroll for moving clips 2025-08-28 23:59:40 +02:00
Maze Winther 2d834a49d7 Merge branch 'staging' 2025-08-28 22:39:19 +02:00
Maze Winther a7d90f7eac feat: copy/pasting for elements 2025-08-28 22:37:51 +02:00
Maze Winther 70a9bfebb1 feat: add audio support for export 2025-08-28 21:52:03 +02:00
Maze Winther d4e08bb7e3 fix: deselect text before timeline selection 2025-08-27 17:55:54 +02:00
Maze Winther 06e3d0ca2a fix: color picker 2025-08-27 17:45:13 +02:00
Maze Winther f9170dcf09 style: re-design colors in text properties panel 2025-08-27 17:23:16 +02:00
Maze Winther ebe1b38bdf style: properties panel 2025-08-27 11:26:28 +02:00
Maze Winther 88c4d160e0 more padding 2025-08-27 01:02:43 +02:00
Maze Winther 163b3599dc padding 2025-08-27 00:56:37 +02:00
Maze Winther adc8cb37fc another huge refactor 2025-08-27 00:52:39 +02:00
Maze Winther b459b46802 refactor: type-safety & separate concerns 2025-08-26 15:04:25 +02:00
Maze Winther 2f7cd11ca1 refactor: type-safety 2025-08-26 11:51:08 +02:00
Maze Winther 9497bef10a Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-26 11:44:06 +02:00
Maze Winther 74d45a7913 style: sonner position 2025-08-26 11:44:00 +02:00
7434f90949 fix: use svh to prevent hero mobile layout shift (#572)
* Update README.md

* fix: use svh to prevent mobile layout shift

---------

Co-authored-by: Maze <167211895+mazeincoding@users.noreply.github.com>
Co-authored-by: Maze Winther <mazewinther@gmail.com>
2025-08-25 21:10:48 +02:00
03e68e77a9 fix(editor): Don't log expected errors in AudioWaveform during unmount (#565)
* Update README.md

* fix(editor): Don't log expected errors in `AudioWaveform` during unmount

---------

Co-authored-by: Maze <167211895+mazeincoding@users.noreply.github.com>
Co-authored-by: Maze Winther <mazewinther@gmail.com>
2025-08-25 11:37:33 +02:00
Maze Winther a4f2273e25 feat: mediabunny + preview canvas refactor 2025-08-23 11:00:59 +02:00
Maze Winther 7756ed0d80 ultracite 2025-08-22 23:02:43 +02:00
Maze Winther 721a28e9a7 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-22 21:08:53 +02:00
Maze Winther fedde33cba claude.md 2025-08-22 21:08:43 +02:00
Maze Winther 0e3c4ef13b chore: gitignore 2025-08-22 21:08:33 +02:00
Megh BariandGitHub ffc1d7526e feat(rename-modal): enhance input visibility in dark theme with border (#561) 2025-08-19 23:24:09 +02:00
DavidandGitHub f750c45765 fix: adjust padding in HeaderBase for landing page (#563) 2025-08-19 23:23:19 +02:00
Maze Winther 792fb6cdc0 Merge branch 'main' of https://github.com/mazeincoding/AppCut 2025-08-18 10:54:56 +02:00
Maze Winther 756a7c7caa feat: implement reveal in media feature 2025-08-18 10:54:28 +02:00
MazeandGitHub 17c2ba2fc8 Update README.md 2025-08-18 00:23:36 +02:00
Maze Winther 13dff382c4 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-17 18:50:25 +02:00
Maze Winther f3ddf26334 feat: export waitlist 2025-08-17 18:50:13 +02:00
enkei64andGitHub 0f1ed40aef fix: stickers panel (#553)
* fixes to the stickers panel

* Reduce padding on the sides to save space
2025-08-16 23:03:16 +02:00
Maze Winther bb461fa9fb refactor: move export button to component 2025-08-16 21:44:23 +02:00
Maze Winther a90a0f9740 typo 2025-08-16 19:16:20 +02:00
Maze Winther 11d2a66c95 style: improve ui for selected timeline elements 2025-08-16 19:11:50 +02:00
Maze Winther 77d8c6c774 style: sort button in /projects 2025-08-16 18:54:18 +02:00
Maze Winther c0709293da style: rounded corners 2025-08-16 18:43:54 +02:00
Maze Winther 9da0dfc107 fix: clear timeline/media on new project creation to prevent state leakage 2025-08-16 18:26:41 +02:00
Maze Winther a0f8e2c1a7 fix: clarify onboarding 2025-08-16 11:46:34 +02:00
Maze Winther 96f3373fad Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-16 11:36:55 +02:00
Maze Winther 354b6caedd fix: improve clarity on export button 2025-08-16 11:36:53 +02:00
karansingh21202andGitHub e96682f89e fix: enable removing background color in text back to transparent and remember last selection (#510) 2025-08-16 07:39:32 +02:00
enkei64andGitHub 1b1486ad94 fix color of icon in footer when in light mode (#552) 2025-08-16 07:37:30 +02:00
Maze Winther f4e127f2d4 feat: export notice dialog 2025-08-15 23:44:19 +02:00
Maze Winther 8c77095268 feat: landing page bg 2025-08-15 23:26:17 +02:00
4621caee19 made header sticky on scroll (#519) (#520)
* made header sticky on scroll (#519)

* cleanup

---------

Co-authored-by: Maze Winther <mazewinther@gmail.com>
2025-08-15 23:22:34 +02:00
7e4cd76762 feat: add an input so the user can go to a specific timestamp (#530)
* feat: add an input so the user can go to a specific timestamp

* fix: use TimeCode type for EditableTimecodeProps

* cleanup

---------

Co-authored-by: Maze Winther <mazewinther@gmail.com>
2025-08-15 23:01:18 +02:00
Maze Winther 033a9aa1bc refactor: duplicated classes 2025-08-15 22:36:48 +02:00
Maze Winther aa5cd1edca refactor: shortcuts help 2025-08-15 22:34:04 +02:00
karansingh21202andGitHub 32d263fccc fix: improve keyboard shortcuts dialog scrolling behavior and cursor (#522) 2025-08-15 02:49:18 +02:00
Maze Winther 7f91c9ca00 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-15 02:12:16 +02:00
Maze Winther 5e8a489a14 refactor: remove test 2025-08-15 02:12:10 +02:00
c3f3345d7b feat: stickers panel (#539)
* Stickers panel base complete

* Improve dark mode stickers background for visibility

* Prevent stickers from being too small or too big

* Improve UI, added credit for Iconify API

* Allow manually loading more collections

* Add a maximum width of 200px so stickers aren't too big

* cleanup

* style: hover state of button

* refactor: input component

* fix: mark input component as client

* so much stuff

---------

Co-authored-by: Maze Winther <mazewinther@gmail.com>
2025-08-15 02:11:21 +02:00
vadi25andGitHub defff2fc46 fix: changed mute track icon to more understandable ones (#550) 2025-08-14 14:30:22 +02:00
Maze Winther 0274fb6626 cleanup 2025-08-13 17:05:17 +02:00
ryuandGitHub 0ae09c9df5 Merge branch 'staging' into feat/timeline-return-to-start 2025-08-13 14:01:07 +05:00
Maze Winther f7fddff1fc Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-13 00:52:22 +02:00
Maze Winther 71b6718337 feat: implement mute video tracks 2025-08-13 00:52:19 +02:00
c72badb7e9 fix missing package on fresh installation (#537)
Co-authored-by: Maze <167211895+mazeincoding@users.noreply.github.com>
2025-08-12 19:39:28 +02:00
f9aab49080 fix hero image link (#544)
Co-authored-by: Maze <167211895+mazeincoding@users.noreply.github.com>
2025-08-12 19:36:22 +02:00
Maze Winther a97eb26fa4 fix: typo causing runtime error 2025-08-12 16:35:01 +02:00
Maze Winther ae987e1d66 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-12 12:38:26 +02:00
Maze Winther 8d3ccf0d56 fix: move landing page image to cdn 2025-08-12 12:38:24 +02:00
Sai TejaandGitHub 3807e146e4 feat(playback): restart from beginning when play pressed at end of timeline (Fixes #536) (#542) 2025-08-12 00:25:52 +02:00
Maze Winther a1e48613b4 feat: add missing environment variables to turbo.json 2025-08-11 22:50:43 +02:00
Maze Winther 665bb9fbd1 fix: typescript errors 2025-08-11 22:29:15 +02:00
Maze Winther 5b059ce6de feat: auto-captions 2025-08-11 22:24:54 +02:00
Ever AlvarezandGitHub 38f8276788 fix: dialog overlay with same z-index that content (#541) 2025-08-11 11:56:43 +02:00
HungVPandGitHub fa698c276c Add FREESOUND API to docker file (#531) 2025-08-09 23:41:23 +02:00
Justin322322andGitHub e908bad167 fix(preview): mute button not affecting embedded video audio, ensure topmost video track renders above lower tracksmute button not affecting embedded video audio (#529) (#532) 2025-08-09 23:39:24 +02:00
Sai TejaandGitHub 8c205273b7 fix(timeline): keep playhead below dialogs (#533) 2025-08-09 23:36:48 +02:00
Maze Winther 88cb750a3b Merge pull request #514 2025-08-09 11:02:15 +02:00
Maze Winther 54e990af26 Merge pull request [#514](https://github.com/mazeincoding/AppCut/issues/514) 2025-08-09 11:02:01 +02:00
ryuandGitHub e71788014d Merge branch 'main' into feat/timeline-return-to-start 2025-08-07 19:37:06 +05:00
Maze Winther effb6a8641 feat: remove background blur note 2025-08-07 15:46:09 +02:00
Maze Winther ce58b5199a feat: external files always create new tracks at playhead position 2025-08-06 12:52:11 +02:00
Maze Winther 2c90ddd5d5 feat: allow external file drops to individual timeline tracks 2025-08-06 12:30:48 +02:00
Maze Winther e5a0deb777 style: move icons on timeline toolbar 2025-08-05 17:18:41 +02:00
Maze Winther 6aaa9a1f58 nvm one more thing 2025-08-05 15:21:26 +02:00
Maze Winther e724a08684 style: complete export button ui 2025-08-05 15:20:42 +02:00
Maze Winther 43c8b59c80 spacing 2025-08-05 15:05:19 +02:00
Maze Winther f8724851fd style: re-design export button (design credit: @AdamBartas on twitter) 2025-08-05 15:03:38 +02:00
Maze Winther 4f6d014286 fix: remaining timeline issues with targeting wrong radix ui scroll element 2025-08-05 14:07:05 +02:00
Maze Winther b83e6bedb7 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-05 12:58:12 +02:00
Mathis BoulaisandGitHub 9c6556ea05 fix: restore horizontal scroll sync after Radix ScrollArea removal (#518) 2025-08-05 12:57:47 +02:00
Maze Winther 2a7cc13d3e fix: ts error 2025-08-05 09:06:13 +02:00
Maze Winther eafea56e99 style: spacing 2025-08-05 08:51:27 +02:00
Maze Winther 539cd45f64 style: input component bg color 2025-08-05 08:49:17 +02:00
Maze Winther efbbc48bc5 refactor: create base view for media panel tabs 2025-08-05 08:46:17 +02:00
Maze Winther 949228f0bb refactor: remove unused imports 2025-08-05 08:26:39 +02:00
Maze Winther e454612000 formatting 2025-08-05 07:25:14 +02:00
Maze Winther dee434cd33 style: padding top in preview toolbar 2025-08-05 07:14:24 +02:00
Maze Winther 974e3636fe Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-05 07:13:13 +02:00
Maze Winther 65b04f5759 ts error 2025-08-05 07:11:03 +02:00
Maze Winther 8bbb41a3cb fix: ts error causing audio waveform signal issue 2025-08-05 06:41:59 +02:00
Maze Winther 1bed89d55f fix: typescript error from old usage 2025-08-05 06:28:45 +02:00
Maze Winther 8f6c487c26 refactor: ultracite cusor rule 2025-08-05 05:47:29 +02:00
Maze Winther b0329f1372 refactor: move canvas size to be a project-level setting instead of editor-level 2025-08-04 20:45:48 +02:00
Maze Winther f0dd30ebfc fix: tiktok blueprint 2025-08-04 20:40:09 +02:00
Maze Winther 1c4c690048 fix: z-index in editor header 2025-08-04 17:22:57 +02:00
Maze Winther f7cd3ebac9 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-04 17:17:51 +02:00
Maze Winther 9ec5a02252 feat: layout guide for tiktok 2025-08-04 17:17:48 +02:00
Pratiyank KumarandGitHub 2fcc5e0643 feat: add tooltip in TabBar (#515) 2025-08-04 14:16:25 +02:00
Maze Winther 0386624ff5 fix: click empty area on timeline to move playhead 2025-08-04 12:35:14 +02:00
Maze Winther efa5be2ab8 feat: proper light mode for hero section 2025-08-04 11:12:44 +02:00
Maze Winther b0cfedbc5a refactor: remove unnecessary code from tab-bar 2025-08-04 10:59:55 +02:00
enkeii64 06d99cb6be fixes 2025-08-04 18:52:52 +10:00
Maze Winther 81de82ffaa feat: new pattern-craft color 2025-08-04 10:37:40 +02:00
enkei64andGitHub 21b248718b Merge branch 'staging' into feature/panel-presets 2025-08-04 18:11:24 +10:00
enkeii64 bb36b2e439 Fix merge conflicts 2025-08-04 17:59:44 +10:00
enkeii64 3a6bc6ebf7 feat: add panel presets 2025-08-04 17:38:02 +10:00
Maze Winther d022a78ae2 Merge branch 'staging' 2025-08-04 03:14:14 +02:00
Maze Winther 4cd8100b6b fix: add missing env variables in turbo.json 2025-08-04 03:13:57 +02:00
Maze Winther 2fddbf071d Merge branch 'main' of https://github.com/mazeincoding/AppCut 2025-08-04 03:00:16 +02:00
Maze Winther d3ace6a946 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-08-04 02:59:52 +02:00
Maze Winther acc2089d0d feat: add license filtering for sound effects 2025-08-04 02:59:07 +02:00
Maze Winther 35ade4b25f refactor: remove unused import 2025-08-04 01:40:24 +02:00
Maze Winther 48b694bbd8 feat: sound effects functionality with freesound's api 2025-08-04 01:30:33 +02:00
Eshan DasandGitHub b52a9ca3e2 fix: preserve line breaks in text element rendering (#461)
- Change whiteSpace from 'nowrap' to 'pre-wrap' in preview panel
- Allows multi-line text content to display correctly
- Maintains existing text wrapping behavior
2025-08-03 14:44:23 +03:00
Maze Winther c0651fec19 feat: replace scroll-area from radix with one that actually works and doesn't cause layout issues 2025-08-03 12:29:06 +02:00
izaandGitHub f50e250ec6 Update CODE_OF_CONDUCT.md (#512) 2025-08-03 12:43:17 +03:00
Maze Winther 02a50d12de feat: empty state on properties panel 2025-08-02 19:22:06 +02:00
Maze Winther 5e2b07e518 feat: better auth rate limiting 2025-08-02 17:33:23 +02:00
Maze Winther c8980de41a refactor: remove all waitlist-related code 2025-08-02 17:28:01 +02:00
Maze Winther ae54aff1a9 style: padding in media panel tabs 2025-08-02 16:12:50 +02:00
Maze Winther 2c0423667c feat: background gradients to project settings 2025-08-02 16:09:20 +02:00
Maze Winther e5a5b76ece style: add hover effect to tabs 2025-08-02 12:37:21 +02:00
Maze Winther 14393c559d fix: grammar 2025-08-02 12:23:30 +02:00
Maze Winther 7891ed2cc1 feat: make settings items expandable 2025-08-02 12:21:24 +02:00
Maze Winther dd09bf2949 feat: add project settings 2025-08-02 01:41:46 +02:00
Maze Winther cd593d1b98 style: global colors 2025-08-02 00:52:31 +02:00
Maze Winther 68da487f30 style: complete re-design of whole editor 2025-08-01 19:57:26 +02:00
Maze Winther 09cb8c8d98 fix: resize not matching with frames 2025-08-01 09:51:54 +02:00
ryu 6412158003 feat: enhance timeline navigation with comprehensive testing and documentation
- Add comprehensive test suite with 13 passing tests
- Create detailed keyboard shortcuts documentation (KEYBOARD_SHORTCUTS.md)
- Add meaningful code comments explaining functionality
- Set up Vitest testing framework with proper mocks
- Fix React imports in timeline components for proper JSX rendering
- Add ResizeObserver mock for browser environment compatibility
- Enhance keyboard shortcuts help to properly format Enter key display

Tests cover:
- Enter key keybinding functionality
- Return to Start button click behavior
- Keyboard shortcuts help system integration
- Component rendering and tooltip structure

Documentation includes:
- Complete keyboard shortcuts reference
- UI feature explanations
- Usage notes and behavior descriptions

All tests passing (13/13) with full feature coverage.
2025-08-01 03:04:06 +05:00
Maze Winther b00d6d5fb6 fix: make timeline element resizing frame based instead of pixel based 2025-07-31 23:47:10 +02:00
ryu f5d5a387d3 feat: add timeline navigation return to start functionality
- Add Enter key shortcut for seeking to timeline start (00:00:00)
- Add return to start button in timeline toolbar with skip-back icon
- Both methods complement existing Home key shortcut
- Update keyboard shortcuts help to display Enter key properly
- Maintain consistent UI styling with existing playback controls

Resolves #415
2025-08-01 02:33:42 +05:00
Maze Winther 60d4b0c3d6 feat: add discord link to editor header 2025-07-31 23:10:12 +02:00
Maze Winther c5baa3fed1 style: new primary color 2025-07-31 23:04:17 +02:00
Maze Winther 5475cea0de style: fresh look on header 2025-07-31 23:01:02 +02:00
Maze Winther c60ec04f8d style: rename project dialog 2025-07-31 22:27:19 +02:00
Maze Winther 732e9f54a8 Merge pull request #494 2025-07-31 21:47:09 +02:00
Maze Winther 1603dd0e58 fix: set default fps on project creation 2025-07-31 21:47:02 +02:00
Maze Winther 3c64f9cfa2 Merge pull request [#494](https://github.com/mazeincoding/AppCut/issues/494) 2025-07-31 21:46:33 +02:00
Maze Winther 6899450df9 Merge branch 'feat/tailwindcss-v4' into staging 2025-07-31 13:39:47 +02:00
Maze Winther 10afbda9dd Merge pull request #499 2025-07-31 13:20:03 +02:00
Maze Winther 2bde71d4bc style: dialog cancel button 2025-07-31 13:14:58 +02:00
Maze Winther 83b568402a fix: button outline styling 2025-07-31 13:14:25 +02:00
Maze Winther b2cd730fb7 Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-07-31 13:12:02 +02:00
Maze Winther 5f4f3cc6c4 Merge pull request [#499](https://github.com/mazeincoding/AppCut/issues/499) 2025-07-31 13:10:37 +02:00
f255ccb818 feat: add hide or show for media elements, mute or unmute for audio elements (#500)
* feat: create external tools section (#493)

* feat: create external tools section

* fix: better wording

* Feature, added hide or show for media elements, mute or unmute for audio elements both effective on timeline and preview panel, with overlay icon for indicating so and state storing,

* Minor UI vertical separator between magnifier and other buttons

* fixed the AbortError issue in the AudioWaveform component. The error was occurring because of a race condition during cleanup when the component unmounts.

* feat: implement bookmarking functionality in the timeline and project storage

- Added bookmark management methods in the project store for toggling and checking bookmarks.
- Updated the timeline component to display bookmark markers and integrate bookmark actions in the context menu.
- Enhanced the storage service to handle bookmarks in project serialization and deserialization.
- Updated project types to include bookmarks as an array of numbers.

---------

Co-authored-by: Dominik K. <dominik@koch-bautechnik.de>
Co-authored-by: Maze Winther <mazewinther@gmail.com>
2025-07-31 12:50:22 +02:00
naoNao89 a1d2fbd96b [Editor] Prevent excessive project creation from invalid routes
Race conditions in useEffect hook caused navigation to invalid project IDs
to create multiple 'Untitled Project' entries. Users reported accumulating
885+ projects due to this bug.

This change adds global invalid project ID tracking across component remounts
and prevents race conditions through:
- Global invalidProjectIds Set in project store with tracking functions
- isInitializingRef to prevent duplicate initialization attempts
- Proper useEffect cleanup with cancellation tokens
- Specific error handling for 'not found' vs recoverable errors
- Removed problematic activeProject?.id dependency

Testing:
- Add comprehensive test suite (15 tests) with Vitest + React Testing Library
- Include integration tests demonstrating bug and validating fix
- Update CI pipeline to run automated tests

Fixes #182
2025-07-31 05:29:42 +07:00
b229d94a8d add forward and backward buttons to non-fullscreen view (#495)
* feat: create external tools section (#493)

* feat: create external tools section

* fix: better wording

* add forward and backward buttons to non-fullscreen

---------

Co-authored-by: Dominik K. <dominik@koch-bautechnik.de>
Co-authored-by: Maze Winther <mazewinther@gmail.com>
2025-07-30 23:56:10 +02:00
Brandon McConnell cd5642161b Swap background CSS inline style for tailwind utility 2025-07-30 16:54:20 -04:00
Brandon McConnell 48282555e0 Migrate remaining tailwind config pieces to CSS config 2025-07-30 16:53:31 -04:00
Brandon McConnell e3ecf4855a Correct blur-sm ➞ blur 2025-07-30 15:05:03 -04:00
Brandon McConnell dbc1d2e1a3 Refine spacing to match previous spacing
Correct hero spacing changes caused by line-height adjustments in Tailwind v4
2025-07-30 15:03:16 -04:00
Brandon McConnell df1e80b528 Lay groundwork for Tailwind v4 upgrade 2025-07-30 13:15:09 -04:00
Maze Winther e6e08057f5 Merge branch 'main' of https://github.com/mazeincoding/AppCut 2025-07-30 18:25:53 +02:00
Maze Winther f15508df62 feat: make timestamp editable 2025-07-30 17:44:19 +02:00
Vlad 281ec63d7e Fixed BUG Editor cursor does not stop at the end of an audio file
Get duration of content on the timeline to make sure the playhead moves until the end of the timeline content when the content is less than 10 seconds.
Originally it would go all the way to the 10 seconds when timeline content was less than that.
I checked how clipchamp does it and they have a min 10 sec timeline but video timeline plays at length of content, so we are doing it like the pros. timeline is jumpy though will probably work on that.
2025-07-30 10:25:35 -04:00
Dominik K.andGitHub 1c20027d33 feat: create external tools section (#493)
* feat: create external tools section

* fix: better wording
2025-07-30 16:20:44 +03:00
Maze Winther b9e5bdff95 fix: projects loading flicker 2025-07-30 11:55:24 +02:00
Maze Winther 17ab403489 Merge pull request #467 2025-07-30 11:47:54 +02:00
Maze Winther 91c95b12d1 Merge pull request [#467](https://github.com/mazeincoding/AppCut/issues/467) 2025-07-30 11:47:43 +02:00
Maze Winther 397421159b Merge pull request #483 2025-07-30 11:38:37 +02:00
Maze Winther 004fa0c12b Merge pull request [#483](https://github.com/mazeincoding/AppCut/issues/483) 2025-07-30 11:38:21 +02:00
Vlad da0e768b32 [BUG] UI: Inactive click area on the Timeline
-Made the clickable area larger by increasing the container height
-Fixed lint errors.
2025-07-29 22:23:34 -04:00
Maze Winther deda0e366f Merge pull request #482 2025-07-29 23:21:47 +02:00
Maze Winther 118d9a9890 Merge pull request [#482](https://github.com/mazeincoding/AppCut/issues/482) 2025-07-29 23:21:02 +02:00
Maze Winther 95c53e441b Merge pull request #471 2025-07-29 21:13:42 +02:00
naoNao89 726d9cb90b feat: add MOV format support and fix cleanup in VideoExporter
- Add codec configuration for MOV format in buildFFmpegCommand
- Use libx264 video codec and aac audio codec for MOV exports
- Fix cleanup method to properly remove progress event listeners
- Store progressCallback reference to enable proper event listener removal
- Prevent memory leaks by correctly cleaning up FFmpeg event handlers
2025-07-30 00:27:08 +07:00
naoNao89 fbff145036 fix: use export type for type exports in export-utils
- Change ExportOptions from interface to type export
- Change ExportProgress from interface to type export
- Addresses CodeRabbit warning about using export type for type exports
2025-07-30 00:14:18 +07:00
naoNao89 073cb817d3 [UI] Fix dialog accessibility and export functionality
- Add DialogTitle with VisuallyHidden to CommandDialog and onboarding
- Replace YouTube redirect with functional VideoExporter implementation
- Create VisuallyHidden component for screen reader accessibility

Resolves GitHub issue #477 (export redirecting to YouTube)
Fixes console warnings about DialogContent accessibility compliance
2025-07-29 21:42:22 +07:00
Maze 2cf02dd90a Merge pull request [#471](https://github.com/mazeincoding/AppCut/issues/471) 2025-07-29 12:46:01 +02:00
MazeandGitHub 2e0c9f4b23 Merge pull request #472 from RohitChandel23/fix/462-select-all-focus-bug
Fix: Prevent Ctrl+A from selecting timeline when input is focused
2025-07-28 00:13:07 +02:00
rohit f6f26fc635 Fix: Prevent Ctrl+A from selecting timeline when input is focused 2025-07-27 23:52:59 +05:30
antonio-jb ef5d8c25eb fix: remove thick black border from checkbox for cleaner UI 2025-07-27 17:56:32 +02:00
antonio-jb df3b092010 fix: remove thick black border from checkbox for cleaner UI 2025-07-27 17:03:33 +02:00
pratiyankkumar 609e115977 fix: sync between slider and input and add input validation 2025-07-27 12:58:44 +05:30
pratiyankkumar 6ab9f25658 feat: add text styling options (bold, italic, underline, strikethrough) and opacity control in text properties panel 2025-07-27 10:27:40 +05:30
Maze Winther d154937ac2 feat: lock playhead to frame (instead of floating) and add autoscroll to timeline 2025-07-26 22:25:57 +02:00
Maze Winther 906f95399d refactor: better error handling 2025-07-26 13:25:10 +02:00
Maze Winther 14475dc9a6 fix: prevent resizing elements on the timeline further than the timeline start 2025-07-26 13:13:27 +02:00
Maze Winther f149ab3b6c Merge pull request #433 2025-07-26 12:54:56 +02:00
Maze Winther 6dfc80cd70 Merge pull request [#433](https://github.com/mazeincoding/AppCut/issues/433) 2025-07-26 12:54:50 +02:00
Maze Winther 043bf7d70a fix: ensure clicking context menu item doesn't move timeline playhead 2025-07-26 12:34:57 +02:00
Maze Winther 317ee9141e style: make context menus appear above playhead 2025-07-26 12:27:28 +02:00
Maze Winther f312d758da feat: undo for when an element gets deleted from media item deletion 2025-07-26 12:24:18 +02:00
Maze Winther 2a1d9f3c34 Merge pull request #423 2025-07-26 12:22:40 +02:00
Maze Winther 309a2ecb61 Merge pull request [#423](https://github.com/mazeincoding/AppCut/issues/423) 2025-07-26 12:22:12 +02:00
Maze Winther 4bcefb1a6f Merge pull request #437 2025-07-26 12:03:13 +02:00
Maze Winther b0bcca18f8 Merge pull request [#437](https://github.com/mazeincoding/AppCut/issues/437) 2025-07-26 12:02:50 +02:00
Maze Winther 76ee723c37 refactor: use reusable PropertyItem component instead of local 2025-07-26 11:53:34 +02:00
Maze Winther ae94056952 fix: scroll in media panel 2025-07-26 11:50:12 +02:00
Maze Winther 7d0113d366 Merge pull request #444 2025-07-26 11:36:11 +02:00
Maze Winther a4f1a02cea Merge pull request [#444](https://github.com/mazeincoding/AppCut/issues/444) 2025-07-26 11:35:52 +02:00
Maze Winther c51e82db9b Merge pull request #446 2025-07-26 11:30:58 +02:00
Maze Winther 00bcce8eca Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-07-26 11:29:30 +02:00
Maze Winther 70fe5cacd1 Merge pull request [#457](https://github.com/mazeincoding/AppCut/issues/457) 2025-07-26 11:29:19 +02:00
MazeandGitHub 87a8ac90e5 Merge pull request #454 from dipanshurdev/text-feat
feat: update text color
2025-07-26 11:25:04 +02:00
MazeandGitHub 4f60c0d354 Merge pull request #457 from itsobi/fix/properties-panel-gets-cut-off-on-smaller-browser-widths
Update PropertyItem component and frame rate dropdown to flex-col so label and value are not on the same line
2025-07-26 11:24:41 +02:00
Maze Winther c34f3994d9 style: spacing in header 2025-07-26 11:22:22 +02:00
Maze Winther 8c72b75bb6 Merge pull request #459 2025-07-26 11:20:01 +02:00
Maze Winther a369b606a5 Merge pull request [#459](https://github.com/mazeincoding/AppCut/issues/459) 2025-07-26 11:19:48 +02:00
Bhavesh Rawat 4e0d2c20f6 handlers optimised 2025-07-26 03:06:18 +05:30
Maze Winther 642f51dac5 Merge pull request #438 2025-07-25 19:18:28 +02:00
Maze Winther 812bab4edf Merge pull request [#438](https://github.com/mazeincoding/AppCut/issues/438) 2025-07-25 19:18:10 +02:00
pratiyankkumar b288cc0eee fix: increase z-index of playhead for improved visibility over timeline while dragging 2025-07-25 20:58:57 +05:30
Kha NguyenandGitHub c73df818d2 Merge branch 'main' into fix/project-title-styling 2025-07-25 10:04:00 -05:00
Maze Winther 411fcaccf1 fix: redirect new tab 2025-07-25 16:57:20 +02:00
Maze Winther 5946b84b4b feat: export redirect 2025-07-25 16:53:04 +02:00
Maze Winther d667c1095c Merge branch 'staging' of https://github.com/mazeincoding/AppCut into staging 2025-07-25 15:34:53 +02:00
Maze Winther 9a18a7a4e9 feat: add toast for export 2025-07-25 15:30:47 +02:00
itsobi e3504de1f1 update PropertyItem component and frame rate dropdown to flex-col so label and value are not on the same line 2025-07-25 07:56:05 -05:00
Dipanshu Rawat 7e4a4e2095 feat: update text color 2025-07-25 14:39:42 +05:30
Kha Nguyen 8dcbbfa252 Merge branch 'main' into fix/project-title-styling 2025-07-24 17:10:39 -05:00
Kha Nguyen 8e737d87e0 Revert "merge main"
This reverts commit 1ccacf6bb5.
2025-07-24 17:04:27 -05:00
Kha Nguyen 1ccacf6bb5 merge main 2025-07-24 17:00:49 -05:00
Hyteq 65c3f9f7f7 fix: global biome exe, fix missing args 2025-07-25 00:55:39 +03:00
Kha Nguyen 262a7d53d6 Merge branch 'main' into fix/improve-shortcuts 2025-07-24 16:50:14 -05:00
Hayden BleaselandGitHub 5931ddb77a feat: add ultracite (#452)
* Init ultracite

* Update scripts and biome.jsonc

* Update biome.jsonc

* Update biome.jsonc

* Update biome.jsonc

* Run format
2025-07-25 00:41:34 +03:00
Kha Nguyen 5a7745f806 chore: remove unused variables and clean up code 2025-07-24 00:06:57 -05:00
Kha Nguyen e8dc0f62da Merge branch 'main' into fix/improve-shortcuts 2025-07-23 23:47:18 -05:00
zelkim c1b6400612 fix: changed overflow-hidden to overflow-scroll 2025-07-24 10:05:49 +08:00
zelkim 4b882d005f fix: changed overflow-hidden to overflow-scroll 2025-07-24 10:00:33 +08:00
Kha Nguyen 0b7a943de3 chore: format code 2025-07-23 20:27:38 -05:00
Kha Nguyen b8671eb761 fix: use button for editable project title on header 2025-07-23 17:27:44 -05:00
Kha Nguyen 1504b4f60c Merge branch 'main' into fix/project-title-styling 2025-07-23 17:10:15 -05:00
Kha Nguyen 7bd952b080 fix: add breadcrumb to editor header 2025-07-23 17:08:56 -05:00
Kha Nguyen b208f355e2 render header components as is
left, center, and right parts should already have wrappers for more
control
2025-07-23 17:02:31 -05:00
Notoriousbrain 9e711c1e09 added the error handling 2025-07-24 03:28:35 +05:30
Notoriousbrain 8be8f0a5a5 added the dialog before deleting 2025-07-24 03:20:00 +05:30
pratiyankkumar 8208d931d0 fix: Added z-index in playhead so it doesnt appears above popups 2025-07-23 15:36:38 +05:30
Karan Singh b87f470dc4 feat: implement smooth text dragging with boundary constraints 2025-07-23 14:17:42 +05:30
Kha Nguyen e0fac61a91 fix: use grid for better project title responsiveness and ellipsis 2025-07-23 00:37:14 -05:00
Kha Nguyen 92079777b9 change reset default button label 2025-07-22 19:47:26 -05:00
Kha Nguyen f6073c47c6 highlight all keys when editing shorcuts for an action 2025-07-22 19:33:10 -05:00
Kha Nguyen f98651cd33 fix: add reset button for custom shorcuts 2025-07-22 19:19:43 -05:00
Kha Nguyen 771591bdfa fix: better detect letter keys using KeyboardEvent.code 2025-07-22 19:19:43 -05:00
Kha Nguyen f3d5eb9d08 refactor: clean up key mapping logic for human-readable shortcuts 2025-07-22 19:19:43 -05:00
Kha Nguyen a434459a9c display key based on OS 2025-07-22 18:54:32 -05:00
Kha Nguyen 901ddb509d fix: prevent action to perform on key press when edit keybindings 2025-07-22 18:54:32 -05:00
Notoriousbrain 4fb6018cf4 changed it to const for loop in removeMediaItem 2025-07-22 17:54:56 +05:30
Notoriousbrain ec87526842 changed the code to make sure it doesnt skip any media 2025-07-22 17:24:40 +05:30
Notoriousbrain cf6ef8f1c4 fix: removed the media from timeline after its deleted 2025-07-22 16:44:16 +05:30
637 changed files with 70824 additions and 23424 deletions
+172
View File
@@ -0,0 +1,172 @@
# Code Review Checklist
Review every point below carefully to ensure files follow consistent code style and best practices.
---
## Function Signatures & Parameters
- [ ] Every function accepts a single object parameter with destructuring in the signature (for readability and future extensibility)
- Exception: tiny one-liner callbacks (e.g. `array.find(x => ...)`, `map`, `filter`, `sort`) do not need destructuring if it hurts readability
```tsx
// ❌ wrong
function formatTime(seconds: number, fps: number) { ... }
// ✅ correct
function formatTime({ seconds, fps }: { seconds: number; fps: number }) { ... }
```
## TypeScript & Type Safety
- [ ] No `any` references
- [ ] General interfaces are in the `types` folder, not scattered in components
- Example: `TimelineTrack` interface belongs in `src/types/timeline.ts`, not `src/components/timeline/index.tsx`
## JSX & Components
- [ ] JSX is clean — no comments explaining what each part does
- [ ] Complex/reusable JSX is extracted into sub-components (placed below the main component)
- [ ] Components shared across multiple files are in separate files
- [ ] File order: constants specific to file (top) -> utils specific to file -> main component → sub-components (bottom)
- [ ] Components render UI only — domain logic lives in hooks, utilities, or managers
- Simple interaction logic (gestures, modifier keys) can stay if not complex
## Code Organization & File Structure
- [ ] Each file has one single purpose/responsibility
- Example: `timeline/index.tsx` should not define `validateElementTrackCompatibility` — that belongs in a lib file
- Example: `lib/timeline-utils.ts` should not declare `TRACK_COLORS` — that belongs in `constants/`
- [ ] File name accurately reflects what the file contains — a misleading name is a bug waiting to happen
- [ ] Business logic lives in either `src/lib`, `src/core` or `src/services` folder
## Comments
- [ ] No AI comments — only human comments that explain _why_, not _what_
- Bad: changelog-style comments, explaining readable code, using more words than necessary
- [ ] All comments are lowercase
## Naming Conventions
- [ ] Readability over brevity — use `element` not `el`, `event` not `e`
- [ ] Booleans are named `isSomething`, `hasSomething`, or `shouldSomething` — not `something`
- [ ] No title case for multi-word text/UI — use `Hello world` not `Hello World`
## Tailwind & Styling
- [ ] Always use `cn()` for `className` — never string interpolation with `${}` or ternaries inline
```tsx
// ❌ wrong
className={`base-class ${isActive && "active"} ${someHelper()}`}
// ✅ correct
className={cn("base-class", isActive && "active", someHelper())}
```
- [ ] Use `gap-*` instead of `mb-*` or `mt-*` for consistent spacing
- [ ] Use `size-*` instead of `h-* w-*` when width and height are the same
- [ ] When using `size-*` on icons inside `<Button>`, use `!` modifier to override default `size-4`
```tsx
<Button>
<PlusIcon className="!size-6" /> {/* ✅ correct */}
<PlusIcon className="size-6" /> {/* ❌ wrong */}
<PlusIcon className="!size-4" /> {/* ❌ unnecessary, size-4 is default */}
<PlusIcon className="size-4" />{" "}
{/* ❌ completely wrong, 1) doesn't override and 2) size-4 is default */}
</Button>
```
## State Management (Zustand)
- [ ] React components never use `someStore.getState()` — use the `useSomeStore` hook instead
- [ ] High-frequency stores (timeline, playback, selections) use selectors — `useStore((s) => s.value)` not `const { value } = useStore()`
- [ ] Store/manager methods are not passed as props — sub-components access them directly
```tsx
// ❌ wrong
function Parent() {
const { selectedElements } = useTimelineStore();
return <Child selectedElements={selectedElements} />;
}
// ✅ correct
function Parent() {
return <Child />;
}
function Child() {
const { selectedElements } = useTimelineStore();
}
```
- [ ] Components and hooks should use the `useEditor` hook. Only use `EditorCore.getInstance()` if you are outside of a react component/hook. Eg: in a utility function, event handler.
## Code Quality
- [ ] Code is scannable — use variables and helper functions to make intent clear at a glance
- [ ] Complex logic is extracted into well-named variables or helpers
- [ ] No magic numbers or magic values — extract inline literals into named constants
- Applies to colors, durations, thresholds, sizes, config values, etc.
- If it's domain-specific to one file, a `const` at the top of that file is fine
- If it's generic enough, it belongs in `constants/`
- [ ] No redundant single/plural function variants — if a function can operate on multiple items, it should accept an array and handle both cases. Don't create `doThing()` + `doThings()`.
```tsx
// ❌ wrong — redundant variants
function updateElement({ element }: { element: Element }) { ... }
function updateElements({ elements }: { elements: Element[] }) { ... }
// ✅ correct — one function, accepts array
function updateElements({ elements }: { elements: Element[] }) { ... }
```
---
## Function Keywords
| Context | Keyword |
| --------------------------------- | ------------------------- |
| Next.js page components | `export default function` |
| Main react component | `export function` |
| Sub-components | `function` |
| Utility functions | `export function` |
| Functions inside react components | `const` |
---
## Review Methodology
Do NOT review by reading the file top-to-bottom and noting what jumps out. Instead:
1. Go through each checklist section **one at a time**
2. For each section, scan the **entire file** for violations of that specific rule
3. Only move to the next section after you've exhausted the current one
4. After all sections are checked, do a final pass: re-read every checklist item and confirm you didn't skip it
Before outputting the review, list each checklist section and confirm you checked it:
`Signatures ✓ | TypeScript ✓ | JSX ✓ | Organization ✓ | File Names ✓ | Comments ✓ | Naming ✓ | Tailwind ✓ | State ✓ | Quality ✓ | Keywords ✓`
---
## IMPORTANT: Review Rules
- **ONLY** flag issues that are explicitly covered by a checklist item above.
- Do **NOT** invent your own rules, suggestions, or "nice to haves" as primary issues.
- The review output must be a list of issues, each one mapping to a specific checklist item.
- If something looks off but isn't covered by the checklist, you can mention it as a brief side note at the end — but keep it clearly separate from the actual review. Always default to fixing the issues covered by the checklist above, unless the user says otherwise.
> You WILL miss things if you try to review the whole file in one pass. Iterate rule by rule.
---
## Think Bigger
After the checklist review, step back and ask the hard questions. The biggest architectural problems get solved by the biggest questions.
- Does this abstraction actually need to exist? Could it be deleted entirely?
- Is this the right layer for this logic? (wrong layer = future pain)
- Is this solving a real problem, or a problem we invented?
- Would a simpler data model make this whole file unnecessary?
- Are we adding complexity to work around a bad decision made earlier?
- Could this field be derived from other existing fields? Redundant data in a model is a source of bugs.
Don't be shy about flagging these. A "why does this exist?" question is often worth more than 10 style fixes.
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
---
alwaysApply: false
---
# Comment Guidelines
## Good Comments (Human-style)
- Explain WHY, not WHAT
- Document non-obvious behavior or edge cases
- Warn about performance implications or side effects
- Explain business logic that isn't clear from code
Examples:
```javascript
// transfer, not copy; sender buffer detaches
// satisfies: check shape; keep literals
// keep multibyte across chunks
// timingSafeEqual throws on length mismatch
```
## Bad Comments (AI-style)
- Don't explain what the code literally does
- Don't add changelog-style comments in code
- Don't comment every line or obvious operations
Avoid:
```javascript
// Prevent duplicate initialization
// Check if project is already loaded
// Mark as initializing to prevent race conditions
// (changed from blah to blah)
```
## Rule
Only add comments when there's genuinely non-obvious behavior, performance considerations, or business logic that needs context. Code should be self-documenting through naming and structure.
+20
View File
@@ -0,0 +1,20 @@
---
alwaysApply: false
---
# Handling Uncertainty
## Principle
If you can't confidently respond due to missing context, data access, or ambiguity (multiple interpretations), report it instead of guessing. Seek clarification to avoid errors.
Apply when: query lacks details, no access to info/tools, or unclear intent.
## How to Report
1. **Description**: Why uncertain and what you need.
2. **Questions**: 1-3 targeted ones.
3. **Assumptions** (opt.): State if proceeding; omit otherwise.
Direct and concise.
**Assumptions**: None.
Builds transparency.
+8
View File
@@ -0,0 +1,8 @@
---
alwaysApply: false
---
# Readability First
Optimize code for AI agents to understand and modify.
Never abbreviate. `event` not `e`, `element` not `el`. If it's easy to read, it's correct. In this case, "config" is better than "configuration" because it's shorter and is **still very readable**. "El" is not very readable.
+51
View File
@@ -0,0 +1,51 @@
---
alwaysApply: false
---
# Separation of Concerns
## Core Principle
Each file should have one single purpose/responsibility. Related functionality should be grouped together, unrelated functionality should be separated.
## Good Separation
- One file per major concern (auth, validation, data transformation)
- Group related utilities together
- Extract shared logic into dedicated files
- Keep API routes focused on their specific endpoint logic
Examples:
```javascript
// ✅ Good: Each file has clear responsibility
/lib/rate-limit.ts // Rate limiting utilities
/lib/validation.ts // Input validation schemas
/lib/freesound-api.ts // External API integration
/api/sounds/search/route.ts // Route handler only
```
## Bad Mixing of Concerns
Avoid cramming multiple responsibilities into one file:
```javascript
// ❌ Bad: Route file doing everything
/api/sounds/search/route.ts
- Rate limiting logic
- Validation schemas
- API transformation
- External API calls
- Response formatting
- Error handling utilities
```
## When to Separate
- File is getting long (>500 lines)
- Multiple distinct responsibilities in one file
- Logic could be reused elsewhere
- Complex utilities that distract from main purpose
## Rule
One file, one responsibility. Extract shared concerns into focused utility files
+101
View File
@@ -0,0 +1,101 @@
---
alwaysApply: false
---
# Project Context
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's formatter.
## 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.
- 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 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);
}
```
+52
View File
@@ -0,0 +1,52 @@
---
alwaysApply: false
---
# Scannable Code Guidelines/Separating Concerns.
## Core Principle
Code should be scannable through proper abstraction, not comments. Use variables and helper functions to make intent clear at a glance.
## Good Scannable Code
- Extract complex logic into well-named variables
- Create helper functions for multi-step operations
- Use descriptive names that explain intent
Examples:
```javascript
// ✅ Scannable: Intent is clear from variable names
const isValidUser = user.isActive && user.hasPermissions;
const shouldProcessPayment = amount > 0 && !order.isPaid;
// ✅ Scannable: Complex logic extracted to helper
const searchParams = buildFreesoundSearchParams({ query, filters, pagination });
const transformedResults = transformFreesoundResults({ rawResults });
```
## Bad Unscannable Code
Avoid:
```javascript
// ❌ Hard to scan: What does this condition mean?
if (type === "effects" || !type) {
params.append("filter", "duration:[* TO 30.0]");
params.append("filter", `avg_rating:[${min_rating} TO *]`);
if (commercial_only) {
params.append("filter", 'license:("Attribution" OR "Creative Commons 0")');
}
}
// ❌ Hard to scan: Complex ternary
const sortParam = query
? sort === "score"
? "score"
: `${sort}_desc`
: `${sort}_desc`;
```
## Rule
Make code scannable by extracting intent into variables and helper functions. If you need to think about what code does, extract it. The reader should understand the flow without diving into implementation details.
+7
View File
@@ -0,0 +1,7 @@
{
"plugins": {
"figma": {
"enabled": true
}
}
}
+45
View File
@@ -0,0 +1,45 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
-1
View File
@@ -1 +0,0 @@
!apps/web/.env.example
+15
View File
@@ -0,0 +1,15 @@
node_modules
.next
.git
.gitignore
*.md
.env*
!.env.example
.cursor
.vscode
diffs
docs
agent-transcripts
mcps
terminals
apps/web/.font-cache
+12 -13
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
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,
- 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
- 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
- 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,
- 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
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -59,8 +59,7 @@ 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
[INSERT CONTACT METHOD].
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
@@ -87,4 +86,4 @@ 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
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+12 -2
View File
@@ -1,5 +1,15 @@
# Contributing to OpenCut
⚠️ We are currently NOT accepting feature PRs while we build out the core editor.
If you want to contribute:
1. Open an issue first to discuss
2. Wait for maintainer approval
3. Only then start coding
Critical bug fixes may be accepted on a case-by-case basis.
Thank you for your interest in contributing to OpenCut! This document provides guidelines and instructions for contributing.
## Getting Started
@@ -98,11 +108,11 @@ If you're unsure whether your idea falls into the preview category, feel free to
```bash
# Database (matches docker-compose.yaml)
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
DATABASE_URL="postgresql://opencut:opencut@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"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
# Redis (matches docker-compose.yaml)
UPSTASH_REDIS_REST_URL="http://localhost:8079"
+3 -3
View File
@@ -1,6 +1,6 @@
name: Bug report
description: Create a report to help us improve
title: '[BUG] '
title: "[BUG] "
labels: bug
body:
- type: input
@@ -15,7 +15,7 @@ body:
id: Browser
attributes:
label: Browser
description: Please enter the browser on which you encountered the bug.
description: Please enter the browser on which you encountered the bug.
placeholder: e.g. Chrome 137, Firefox 137, Safari 17
validations:
required: true
@@ -67,4 +67,4 @@ body:
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
required: false
+2 -2
View File
@@ -1,6 +1,6 @@
name: Feature request
description: Suggest an idea for OpenCut
title: '[FEATURE] '
title: "[FEATURE] "
labels: enhancement
body:
- type: markdown
@@ -39,4 +39,4 @@ body:
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
required: false
+1 -1
View File
@@ -25,4 +25,4 @@ Please do not report security vulnerabilities through public GitHub issues.
- 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!
Thank you for helping keep OpenCut secure!
+5 -1
View File
@@ -3,21 +3,25 @@
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!
We appreciate your patience and contributions to making OpenCut better!
+344
View File
@@ -0,0 +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);
}
```
+7 -45
View File
@@ -1,49 +1,11 @@
## Description
⚠️ READ BEFORE SUBMITTING ⚠️
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
We are not currently accepting PRs except for critical bugs.
Fixes # (issue)
If this is a bug fix:
- [ ] I've opened an issue first
- [ ] This was approved by a maintainer
## Type of change
If this is a feature:
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.
This PR will be closed. Please open an issue to discuss first.
+12 -3
View File
@@ -22,12 +22,21 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
env:
DATABASE_URL: "postgresql://opencut:opencutthegoat@localhost:5432/opencut"
DATABASE_URL: "postgresql://opencut:opencut@localhost:5432/opencut"
BETTER_AUTH_SECRET: "supersecret"
NEXT_PUBLIC_BETTER_AUTH_URL: "http://localhost:3000"
NEXT_PUBLIC_SITE_URL: "http://localhost:3000"
UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url"
UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token"
NEXT_PUBLIC_MARBLE_API_URL: "https://placeholder.example.com"
MARBLE_WORKSPACE_KEY: "placeholder"
FREESOUND_CLIENT_ID: "placeholder"
FREESOUND_API_KEY: "placeholder"
CLOUDFLARE_ACCOUNT_ID: "placeholder"
R2_ACCESS_KEY_ID: "placeholder"
R2_SECRET_ACCESS_KEY: "placeholder"
R2_BUCKET_NAME: "placeholder"
MODAL_TRANSCRIPTION_URL: "https://placeholder.example.com"
steps:
- name: Checkout repository
uses: actions/checkout@v4
+8 -20
View File
@@ -1,22 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# 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
@@ -24,9 +7,14 @@ node_modules
.cursorignore
.turbo
*.env
.env*
!*.env.example
# cursor
bun.lockb
.cursor/
bun.lockb
# content-collections
.content-collections
# Twiggy
.cursor/rules/file-structure.mdc
+20
View File
@@ -0,0 +1,20 @@
{
"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"
}
}
+121
View File
@@ -0,0 +1,121 @@
# AGENTS.md
## Overview
Privacy-first video editor, with a focus on simplicity and ease of use.
## Lib vs Utils
- `lib/` - domain logic (specific to this app)
- `utils/` - small helper utils (generic, could be copy-pasted into any other app)
## Core Editor System
The editor uses a **singleton EditorCore** that manages all editor state through specialized managers.
### Architecture
```
EditorCore (singleton)
├── playback: PlaybackManager
├── timeline: TimelineManager
├── scene: SceneManager
├── project: ProjectManager
├── media: MediaManager
└── renderer: RendererManager
```
### When to Use What
#### In React Components
**Always use the `useEditor()` hook:**
```typescript
import { useEditor } from '@/hooks/use-editor';
function MyComponent() {
const editor = useEditor();
const tracks = editor.timeline.getTracks();
// Call methods
editor.timeline.addTrack({ type: 'media' });
// Display data (auto re-renders on changes)
return <div>{tracks.length} tracks</div>;
}
```
The hook:
- Returns the singleton instance
- Subscribes to all manager changes
- Automatically re-renders when state changes
#### Outside React Components
**Use `EditorCore.getInstance()` directly:**
```typescript
// In utilities, event handlers, or non-React code
import { EditorCore } from "@/core";
const editor = EditorCore.getInstance();
await editor.export({ format: "mp4", quality: "high" });
```
## Actions System
Actions are the trigger layer for user-initiated operations. The single source of truth is `@/lib/actions/definitions.ts`.
**To add a new action:**
1. Add it to `ACTIONS` in `@/lib/actions/definitions.ts`:
```typescript
export const ACTIONS = {
"my-action": {
description: "What the action does",
category: "editing",
defaultShortcuts: ["ctrl+m"],
},
// ...
};
```
2. Add handler in `@/hooks/use-editor-actions.ts`:
```typescript
useActionHandler(
"my-action",
() => {
// implementation
},
undefined,
);
```
**In components, use `invokeAction()` for user-triggered operations:**
```typescript
import { invokeAction } from '@/lib/actions';
// Good - uses action system
const handleSplit = () => invokeAction("split-selected");
// Avoid - bypasses UX layer (toasts, validation feedback)
const handleSplit = () => editor.timeline.splitElements({ ... });
```
Direct `editor.xxx()` calls are for internal use (commands, tests, complex multi-step operations).
## Commands System
Commands handle undo/redo. They live in `@/lib/commands/` organized by domain (timeline, media, scene).
Each command extends `Command` from `@/lib/commands/base-command` and implements:
- `execute()` - saves current state, then does the mutation
- `undo()` - restores the saved state
Actions and commands work together: actions are "what triggered this", commands are "how to do it (and undo it)".
+38 -94
View File
@@ -1,19 +1,31 @@
<table width="100%">
<tr>
<td align="left" width="120">
<img src="apps/web/public/logo.png" alt="OpenCut Logo" width="100" />
<img src="apps/web/public/logos/opencut/1k/logo-white-black.png" alt="OpenCut Logo" width="100" />
</td>
<td align="right">
<h1>OpenCut <span style="font-size: 0.7em; font-weight: normal;">(prev AppCut)</span></h1>
<h1>OpenCut</span></h1>
<h3 style="margin-top: -10px;">A free, open-source video editor for web, desktop, and mobile.</h3>
</td>
</tr>
</table>
## Sponsors
Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
<a href="https://vercel.com/oss">
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
</a>
<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>
## Why?
- **Privacy**: Your videos stay on your device
- **Free features**: Every basic feature of CapCut is paywalled now
- **Free features**: Most basic CapCut features are now paywalled
- **Simple**: People want editors that are easy to use - CapCut proved that
## Features
@@ -38,110 +50,52 @@
### Prerequisites
Before you begin, ensure you have the following installed on your system:
- [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 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!
> **Note:** Docker is optional but recommended for running the local database and Redis. If you only want to work on frontend features, you can skip it.
### 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`:
1. Fork and clone the repository
2. Copy the environment file:
```bash
# Unix/Linux/Mac
cp .env.example .env.local
# Windows Command Prompt
copy .env.example .env.local
cp apps/web/.env.example apps/web/.env.local
# Windows PowerShell
Copy-Item .env.example .env.local
Copy-Item apps/web/.env.example apps/web/.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:
3. Start the database and Redis:
```bash
# From project root
docker-compose up -d
docker compose up -d db redis serverless-redis-http
```
2. Navigate to the web app directory:
4. Install dependencies and start the dev server:
```bash
cd apps/web
bun install
bun dev: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).
The `.env.example` has sensible defaults that match the Docker Compose config — it should work out of the box.
### Self-Hosting with Docker
To run everything (including a production build of the app) in Docker:
```bash
docker compose up -d
```
The app will be available at [http://localhost:3100](http://localhost:3100).
## Contributing
We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
@@ -158,16 +112,6 @@ See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instruc
- Follow the setup instructions in CONTRIBUTING.md
- Create a feature branch and submit a PR
## Sponsors
Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
<a href="https://vercel.com/oss">
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
</a>
[![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)
## License
[MIT LICENSE](LICENSE)
+21 -12
View File
@@ -1,19 +1,28 @@
# Environment Variables Example
# Environment variables example
# Copy this file to .env.local and update the values as needed
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
# Better Auth
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your-secret-key-here
# Development Environment
# Node
NODE_ENV=development
# Redis
# Public
NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
# Server
DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut"
BETTER_AUTH_SECRET=your_better_auth_secret
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
MARBLE_WORKSPACE_KEY=your_workspace_key_here
FREESOUND_CLIENT_ID=your_client_id_here
FREESOUND_API_KEY=your_api_key_here
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
R2_ACCESS_KEY_ID=your_access_key_here
R2_SECRET_ACCESS_KEY=your_secret_key_here
R2_BUCKET_NAME=opencut-transcription # whatever you named your r2 bucket
MODAL_TRANSCRIPTION_URL=your_modal_url_here
+9 -1
View File
@@ -1,2 +1,10 @@
# Turborepo
.turbo
.turbo
# Env vars
.env*
!.env.example
.next/
.font-cache/
+70 -57
View File
@@ -1,57 +1,70 @@
FROM oven/bun:alpine AS base
# Install dependencies and build the application
FROM base AS builder
WORKDIR /app
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"
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"]
FROM oven/bun:alpine AS base
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/env/package.json packages/env/package.json
COPY packages/ui/package.json packages/ui/package.json
RUN bun install
COPY apps/web/ apps/web/
COPY packages/env/ packages/env/
COPY packages/ui/ packages/ui/
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Build-time env stubs to pass zod validation
ENV DATABASE_URL="postgresql://opencut:opencut@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_SITE_URL="http://localhost:3000"
ENV NEXT_PUBLIC_MARBLE_API_URL="https://api.marblecms.com"
ENV MARBLE_WORKSPACE_KEY="build-placeholder"
ENV CLOUDFLARE_ACCOUNT_ID="build-placeholder"
ENV R2_ACCESS_KEY_ID="build-placeholder"
ENV R2_SECRET_ACCESS_KEY="build-placeholder"
ENV R2_BUCKET_NAME="build-placeholder"
ENV MODAL_TRANSCRIPTION_URL="http://localhost:0"
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"]
+20 -20
View File
@@ -1,21 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+32
View File
@@ -0,0 +1,32 @@
import { defineCollection, defineConfig } from "@content-collections/core";
import { z } from "zod";
const changelog = defineCollection({
name: "changelog",
directory: "content/changelog",
include: "*.md",
schema: z.object({
version: z.string(),
date: z.string(),
title: z.string(),
description: z.string().optional(),
changes: z.array(
z.object({
type: z.string(),
text: z.string(),
}),
),
}),
transform: async (doc, { collection }) => {
const allDocs = await collection.documents();
const sorted = [...allDocs].sort((a, b) =>
b.version.localeCompare(a.version, undefined, { numeric: true }),
);
const isLatest = sorted[0]?.version === doc.version;
return { ...doc, isLatest };
},
});
export default defineConfig({
content: [changelog],
});
+35
View File
@@ -0,0 +1,35 @@
---
version: "0.1.0"
date: "2026-02-23"
title: "Editor foundation"
description: "This first release focuses on making editing faster, clearer, and more reliable for day-to-day use."
changes:
- type: new
text: "Rebuilt the properties panel from scratch. Number inputs now support math expressions and click-drag scrubbing instead of just typing values in."
- type: new
text: "The properties panel went from a flat list of basic text styles to organized sections: transform, blending, typography, spacing, and background controls. Text elements finally have position, scale, and rotation."
- type: new
text: "New color picker with eyedropper, opacity control, and multiple color formats."
- type: new
text: "Bookmarks now support notes, color labels, and optional duration to plan scenes more clearly."
- type: new
text: "Move, scale, and rotate elements directly in the preview panel."
- type: new
text: "Blend modes (Multiply, Screen, Overlay, etc). Only available on text elements for now."
- type: new
text: "Paste images, videos, or audio from your clipboard straight into the editor."
- type: new
text: "Hold Shift while moving or resizing to temporarily turn off snapping."
- type: improved
text: "Expanded font selection from 7 to over 1,000."
- type: improved
text: "Fonts load much faster."
- type: improved
text: "All asset panel tabs now share a consistent layout."
- type: improved
text: "Text rendering properly supports multiple lines, line height, and letter spacing."
- type: improved
text: "Better contrast in the dark theme."
- type: fixed
text: "Fixed undo/redo for drag-and-drop so dropped items are reliably undoable."
---
+45
View File
@@ -0,0 +1,45 @@
---
version: "0.2.0"
date: "2026-02-30"
title: "Motion & effects"
description: "This release adds the foundation for motion and effects, alongside many improvements & fixes."
changes:
- type: new
text: "Keyframe animation system for animating element properties over time."
- type: new
text: "Entirely new efects system. Effects can be applied per-clip or added as standalone elements on the timeline (with our first effect: Blur!)"
- type: new
text: "Added a changelog page to the site."
- type: new
text: "Ripple editing mode."
- type: fixed
text: "Fixed an issue where click-and-drag selection on one timeline track could accidentally select items from the track below."
- type: fixed
text: "Audio could flicker or cut out at the start of playback when certain elements were selected. The scheduler now recovers from timing slips without losing audio."
- type: fixed
text: "Dragging a clip onto the timeline and pressing Ctrl+Z now removes both the clip and the track it created in one step, instead of requiring two undos."
- type: fixed
text: "A few values in the properties panel were previously showing incorrect values. This has been fixed."
- type: improved
text: "New text elements no longer have a black background by default."
- type: improved
text: "Selection outline in the preview is now dashed."
- type: improved
text: "Adjacent clips on the timeline now have a small gap between them so selected clips are always clearly visible."
- type: fixed
text: "Timeline trim handles now sit outside the clip instead of inside them, so the clip's visible area accurately represents where it starts and ends."
- type: fixed
text: "Clips being dragged on the timeline now render on top of other clips instead of underneath them."
- type: fixed
text: "Undoing a trim-from-start operation now correctly restores the clip to its original position and length, instead of stretching it to the right."
- type: fixed
text: "Resizing a clip on the timeline would deselect it when the mouse was released. The clip now stays selected after a resize."
- type: improved
text: "Press Escape to deselect the current element."
- type: fixed
text: "Resizing a clip could extend it past another clip on the same track, causing an overlap. The resize now stops at the next clip's edge."
- type: fixed
text: "Dragging a clip to a different track would deselect it on drop. The clip stays selected after moving."
- type: fixed
text: "Finishing a resize or drag with the mouse over empty track space would deselect the clip."
---
+23 -19
View File
@@ -1,19 +1,23 @@
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;
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
import { webEnv } from "@opencut/env/web";
// Load the right env file based on environment
if (webEnv.NODE_ENV === "production") {
dotenv.config({ path: ".env.production" });
} else {
dotenv.config({ path: ".env.local" });
}
export default {
schema: "./src/schema.ts",
dialect: "postgresql",
migrations: {
table: "drizzle_migrations",
},
dbCredentials: {
url: webEnv.DATABASE_URL,
},
out: "./migrations",
strict: webEnv.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;
+343 -318
View File
@@ -1,319 +1,344 @@
{
"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": {}
}
}
"id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6",
"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
},
"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": {}
}
}
-358
View File
@@ -1,358 +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": {}
}
}
-359
View File
@@ -1,359 +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": {}
}
}
+12 -26
View File
@@ -1,27 +1,13 @@
{
"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
}
]
}
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1750753385927,
"tag": "0000_brainy_saracen",
"breakpoints": true
}
]
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+52 -31
View File
@@ -1,37 +1,58 @@
import type { NextConfig } from "next";
import { withBotId } from "botid/next/config";
import { withContentCollections } from "@content-collections/next";
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",
},
],
},
turbopack: {
rules: {
"*.glsl": {
loaders: [require.resolve("raw-loader")],
as: "*.js",
},
},
},
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);
export default withContentCollections(withBotId(nextConfig));
+45 -20
View File
@@ -1,10 +1,10 @@
{
"name": "opencut",
"name": "@opencut/web",
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.2.18",
"scripts": {
"dev": "next dev",
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "biome check src/",
@@ -21,40 +21,55 @@
"@ffmpeg/util": "^0.12.2",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^3.9.1",
"@opencut/auth": "workspace:*",
"@opencut/db": "workspace:*",
"@radix-ui/react-separator": "^1.1.7",
"@t3-oss/env-core": "^0.13.8",
"@t3-oss/env-nextjs": "^0.13.8",
"@upstash/ratelimit": "^2.0.5",
"@upstash/redis": "^1.35.0",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@huggingface/transformers": "^3.8.1",
"@opencut/env": "workspace:*",
"@opencut/ui": "workspace:*",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-primitive": "^2.1.4",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/culori": "^4.0.1",
"@upstash/ratelimit": "^2.0.6",
"@upstash/redis": "^1.35.4",
"@vercel/analytics": "^1.4.1",
"aws4fetch": "^1.0.20",
"better-auth": "^1.2.7",
"botid": "^1.4.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"culori": "^4.0.2",
"dayjs": "^1.11.13",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.44.2",
"embla-carousel-react": "^8.5.1",
"eventemitter3": "^5.0.1",
"feed": "^5.1.0",
"framer-motion": "^11.13.1",
"input-otp": "^1.4.1",
"lucide-react": "^0.468.0",
"lucide-react": "^0.562.0",
"mediabunny": "^1.29.1",
"motion": "^12.18.1",
"next": "^15.4.3",
"nanoid": "^5.1.5",
"next": "16.1.3",
"next-themes": "^0.4.4",
"pg": "^8.16.2",
"radix-ui": "^1.4.2",
"react": "^18.2.0",
"postgres": "^3.4.5",
"radix-ui": "^1.4.3",
"react": "^19.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-dom": "^19.0.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",
"react-window": "^2.2.7",
"recharts": "^2.14.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-parse": "^9.0.1",
@@ -65,20 +80,30 @@
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"vaul": "^1.1.1",
"use-deep-compare-effect": "^1.8.1",
"vaul": "^1.1.2",
"wavesurfer.js": "^7.9.8",
"zod": "^3.25.67",
"zustand": "^5.0.2"
},
"devDependencies": {
"@content-collections/core": "^0.14.1",
"@content-collections/next": "^0.2.11",
"@napi-rs/canvas": "^0.1.92",
"@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",
"@types/react": "^19",
"@types/react-dom": "^19",
"cross-env": "^7.0.3",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"raw-loader": "^4.0.2",
"sharp": "^0.34.5",
"tailwindcss": "^4.1.11",
"tsx": "^4.7.1",
"typescript": "^5.8.3"
}
+3 -3
View File
@@ -1,8 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

-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: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 362 B

After

Width:  |  Height:  |  Size: 362 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="512" height="513" viewBox="0 0 512 513" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M330.144 0.740479C339.014 0.740479 346.122 7.95256 346.971 16.7816C354.534 95.534 417.207 158.206 495.959 165.769C504.788 166.618 512 173.726 512 182.596V330.886C512 339.755 504.788 346.863 495.959 347.712C417.207 355.275 354.534 417.947 346.971 496.7C346.122 505.529 339.014 512.74 330.144 512.74H181.856C172.986 512.74 165.878 505.529 165.029 496.7C157.467 417.947 94.7934 355.275 16.041 347.712C7.21212 346.863 0 339.755 0 330.886V182.596C0 173.726 7.21216 166.618 16.0411 165.769C94.7935 158.206 157.467 95.534 165.029 16.7816C165.878 7.95256 172.986 0.740479 181.856 0.740479H330.144ZM102.8 256.307C102.8 341.495 171.781 410.554 256.874 410.554C341.966 410.554 410.948 341.495 410.948 256.307C410.948 171.119 341.966 102.06 256.874 102.06C171.781 102.06 102.8 171.119 102.8 256.307Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 960 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="588" height="512" viewBox="0 0 588 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M294 0L588 512H0L294 0Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 156 B

+43 -43
View File
@@ -1,44 +1,44 @@
{
"name": "OpenCut",
"description": "A simple but powerful video editor that gets the job done. In your browser.",
"display": "standalone",
"start_url": "/",
"icons": [
{
"src": "/icons/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "/icons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"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"
}
]
}
"name": "OpenCut",
"description": "A simple but powerful video editor that gets the job done. In your browser.",
"display": "standalone",
"start_url": "/",
"icons": [
{
"src": "/icons/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "/icons/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"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"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="44" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 126 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon points="50,6 94,50 50,94 6,50" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 135 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon points="28,8 72,8 94,50 72,92 28,92 6,50" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 146 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect x="10" y="10" width="80" height="80" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 138 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon points="50,6 61,37 94,37 67,56 77,88 50,68 23,88 33,56 6,37 39,37" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 171 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon points="50,8 94,92 6,92" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 129 B

+281
View File
@@ -0,0 +1,281 @@
/**
* Generates font sprite atlas for the font picker.
*
* Downloads Google Fonts from Fontsource, renders each font name as a sprite,
* packs them into chunk images, and outputs a JSON atlas + AVIF images.
*
* Run: npx tsx scripts/generate-font-sprites.ts
* Deps: @napi-rs/canvas sharp (install as devDependencies)
*/
import { createCanvas, GlobalFonts } from "@napi-rs/canvas";
import sharp from "sharp";
import { mkdir, writeFile, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const FONT_SIZE = 24;
const ROW_HEIGHT = 40;
const CANVAS_WIDTH = 1200;
const MAX_CHUNK_HEIGHT = 800;
const PADDING_X = 14;
const WIDTH_BUFFER = 8;
const CONCURRENT_DOWNLOADS = 30;
const OUTPUT_DIR = join(__dirname, "..", "public", "fonts");
const CACHE_DIR = join(__dirname, "..", ".font-cache");
const FONTSOURCE_API = "https://api.fontsource.org/v1/fonts";
interface FontsourceFont {
id: string;
family: string;
subsets: string[];
weights: number[];
styles: string[];
category: string;
type: string;
}
interface MeasuredFont {
family: string;
width: number;
styles: string[];
}
interface PackedFont {
family: string;
x: number;
y: number;
w: number;
}
interface AtlasEntry {
x: number;
y: number;
w: number;
ch: number;
s: string[];
}
async function fetchFontList(): Promise<FontsourceFont[]> {
console.log("Fetching font list from Fontsource...");
const response = await fetch(FONTSOURCE_API);
if (!response.ok) throw new Error(`Fontsource API error: ${response.status}`);
const data: FontsourceFont[] = await response.json();
const filtered = data.filter(
(font) =>
font.type === "google" &&
font.subsets.includes("latin") &&
font.weights.includes(400),
);
console.log(
` ${filtered.length} Google fonts with Latin subset + 400 weight`,
);
return filtered;
}
async function downloadFont({ id }: { id: string }): Promise<Buffer | null> {
const cachePath = join(CACHE_DIR, `${id}.woff2`);
if (existsSync(cachePath)) {
return readFile(cachePath);
}
// Fontsource CDN serves woff2 for all Google fonts
const url = `https://cdn.jsdelivr.net/fontsource/fonts/${id}@latest/latin-400-normal.woff2`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const buffer = Buffer.from(await response.arrayBuffer());
await writeFile(cachePath, buffer);
return buffer;
} catch {
return null;
}
}
async function downloadAllFonts({
fonts,
concurrency,
}: {
fonts: FontsourceFont[];
concurrency: number;
}): Promise<Map<string, Buffer>> {
console.log(
`Downloading ${fonts.length} font files (${concurrency} concurrent)...`,
);
const results = new Map<string, Buffer>();
let nextIndex = 0;
let completed = 0;
async function worker() {
while (nextIndex < fonts.length) {
const index = nextIndex++;
const font = fonts[index];
const buffer = await downloadFont({ id: font.id });
if (buffer) results.set(font.id, buffer);
completed++;
if (completed % 100 === 0 || completed === fonts.length) {
process.stdout.write(`\r ${completed}/${fonts.length}`);
}
}
}
await Promise.all(Array.from({ length: concurrency }, () => worker()));
console.log(`\n Downloaded ${results.size}/${fonts.length} fonts`);
return results;
}
function measureFonts({
fonts,
fontBuffers,
}: {
fonts: FontsourceFont[];
fontBuffers: Map<string, Buffer>;
}): MeasuredFont[] {
console.log("Registering fonts and measuring text...");
const measured: MeasuredFont[] = [];
const canvas = createCanvas(CANVAS_WIDTH, ROW_HEIGHT);
const ctx = canvas.getContext("2d");
for (const font of fonts) {
const buffer = fontBuffers.get(font.id);
if (!buffer) continue;
try {
const ok = GlobalFonts.register(buffer, font.family);
if (!ok) continue;
ctx.font = `${FONT_SIZE}px "${font.family}"`;
const metrics = ctx.measureText(font.family);
const width = Math.ceil(metrics.width) + WIDTH_BUFFER;
const styles: string[] = [];
for (const weight of font.weights) {
if (font.styles.includes("normal")) styles.push(String(weight));
if (font.styles.includes("italic")) styles.push(`${weight}i`);
}
measured.push({ family: font.family, width, styles });
} catch {
// skip fonts that fail to register
}
}
measured.sort((a, b) => a.family.localeCompare(b.family));
console.log(` ${measured.length} fonts measured`);
return measured;
}
function packIntoChunks({ measured }: { measured: MeasuredFont[] }): {
atlas: Record<string, AtlasEntry>;
chunks: PackedFont[][];
} {
console.log("Packing into sprite chunks...");
const atlas: Record<string, AtlasEntry> = {};
const chunks: PackedFont[][] = [[]];
let chunkIndex = 0;
let cursorX = 0;
let cursorY = 0;
for (const font of measured) {
// New row if doesn't fit horizontally
if (cursorX + font.width > CANVAS_WIDTH) {
cursorX = 0;
cursorY += ROW_HEIGHT;
}
// New chunk if doesn't fit vertically
if (cursorY + ROW_HEIGHT > MAX_CHUNK_HEIGHT) {
chunkIndex++;
chunks.push([]);
cursorX = 0;
cursorY = 0;
}
// Same data goes to BOTH the atlas and the chunk render list
const x = cursorX;
const y = cursorY;
atlas[font.family] = {
x,
y,
w: font.width,
ch: chunkIndex,
s: font.styles,
};
chunks[chunkIndex].push({ family: font.family, x, y, w: font.width });
cursorX += font.width + PADDING_X;
}
console.log(` ${chunks.length} chunks`);
return { atlas, chunks };
}
async function renderChunks({
chunks,
}: {
chunks: PackedFont[][];
}): Promise<void> {
console.log("Rendering sprite chunks...");
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkHeight = Math.max(...chunk.map((f) => f.y)) + ROW_HEIGHT;
const canvas = createCanvas(CANVAS_WIDTH, chunkHeight);
const ctx = canvas.getContext("2d");
for (const font of chunk) {
ctx.font = `${FONT_SIZE}px "${font.family}"`;
ctx.fillStyle = "#000000";
ctx.textBaseline = "middle";
ctx.fillText(font.family, font.x, font.y + ROW_HEIGHT / 2);
}
const pngBuffer = canvas.toBuffer("image/png");
await sharp(pngBuffer)
.avif({ quality: 80 })
.toFile(join(OUTPUT_DIR, `font-chunk-${i}.avif`));
console.log(
` Chunk ${i}: ${chunk.length} fonts, ${CANVAS_WIDTH}×${chunkHeight}`,
);
}
}
async function main() {
await mkdir(OUTPUT_DIR, { recursive: true });
await mkdir(CACHE_DIR, { recursive: true });
const fonts = await fetchFontList();
const fontBuffers = await downloadAllFonts({
fonts,
concurrency: CONCURRENT_DOWNLOADS,
});
const measured = measureFonts({ fonts, fontBuffers });
const { atlas, chunks } = packIntoChunks({ measured });
await renderChunks({ chunks });
// Write atlas JSON (compact for smaller file size)
await writeFile(
join(OUTPUT_DIR, "font-atlas.json"),
JSON.stringify({ fonts: atlas }),
);
const totalFonts = Object.keys(atlas).length;
console.log(
`\nDone! ${totalFonts} fonts in ${chunks.length} chunks → ${OUTPUT_DIR}`,
);
}
main().catch((error) => {
console.error("Failed:", error);
process.exit(1);
});
-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);
+4 -4
View File
@@ -1,4 +1,4 @@
import { auth } from "@opencut/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
import { auth } from "@/lib/auth/server";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
+2 -4
View File
@@ -1,5 +1,3 @@
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
return new Response("OK", { status: 200 });
export async function GET() {
return new Response("OK", { status: 200 });
}
+280
View File
@@ -0,0 +1,280 @@
import { webEnv } from "@opencut/env/web";
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { checkRateLimit } from "@/lib/rate-limit";
const searchParamsSchema = z.object({
q: z.string().max(500, "Query too long").optional(),
type: z.enum(["songs", "effects"]).optional(),
page: z.coerce.number().int().min(1).max(1000).default(1),
page_size: z.coerce.number().int().min(1).max(150).default(20),
sort: z
.enum(["downloads", "rating", "created", "score"])
.default("downloads"),
min_rating: z.coerce.number().min(0).max(5).default(3),
commercial_only: z.coerce.boolean().default(true),
});
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(),
});
function buildSortParameter({ query, sort }: { query?: string; sort: string }) {
if (!query) return `${sort}_desc`;
return sort === "score" ? "score" : `${sort}_desc`;
}
function applyEffectsFilters({
params,
min_rating,
commercial_only,
}: {
params: URLSearchParams;
min_rating: number;
commercial_only: boolean;
}) {
params.append("filter", "duration:[* TO 30.0]");
params.append("filter", `avg_rating:[${min_rating} TO *]`);
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",
);
}
function transformFreesoundResult(
result: z.infer<typeof freesoundResultSchema>,
) {
return {
id: result.id,
name: result.name,
description: result.description,
url: result.url,
previewUrl:
result.previews?.["preview-hq-mp3"] ||
result.previews?.["preview-lq-mp3"],
downloadUrl: result.download,
duration: result.duration,
filesize: result.filesize,
type: result.type,
channels: result.channels,
bitrate: result.bitrate,
bitdepth: result.bitdepth,
samplerate: result.samplerate,
username: result.username,
tags: result.tags,
license: result.license,
created: result.created,
downloads: result.num_downloads || 0,
rating: result.avg_rating || 0,
ratingCount: result.num_ratings || 0,
};
}
export async function GET(request: NextRequest) {
try {
const { limited } = await checkRateLimit({ request });
if (limited) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
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/";
const sortParam = buildSortParameter({ query, sort });
const params = new URLSearchParams({
query: query || "",
token: webEnv.FREESOUND_API_KEY,
page: page.toString(),
page_size: pageSize.toString(),
sort: sortParam,
fields:
"id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
});
const isEffectsSearch = type === "effects" || !type;
if (isEffectsSearch) {
applyEffectsFilters({ params, min_rating, commercial_only });
}
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(transformFreesoundResult);
const responseData = {
count: data.count,
next: data.next,
previous: data.previous,
results: transformedResults,
query: query || "",
type: type || "effects",
page,
pageSize,
sort,
minRating: min_rating,
};
const 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 },
);
}
}
-83
View File
@@ -1,83 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { db, eq, waitlist } from "@opencut/db";
import { checkBotId } from "botid/server";
import { nanoid } from "nanoid";
import { waitlistRateLimit } from "@/lib/rate-limit";
import { z } from "zod";
import { env } from "@/env";
import { cookies } from "next/headers";
import crypto from "crypto";
const waitlistSchema = z.object({
email: z.string().email("Invalid email format").min(1, "Email is required"),
});
const CSRF_TOKEN_NAME = "waitlist-csrf";
const TOKEN_EXPIRY = 60 * 60 * 1000;
async function validateCSRFToken(request: NextRequest): Promise<boolean> {
const clientToken = request.headers.get("x-csrf-token");
if (!clientToken) return false;
const cookieStore = await cookies();
const cookieValue = cookieStore.get(CSRF_TOKEN_NAME)?.value;
if (!cookieValue) return false;
const [token, timestamp, signature] = cookieValue.split(":");
if (!token || !timestamp || !signature) return false;
if (clientToken !== token) return false;
const now = Date.now();
const tokenTime = parseInt(timestamp);
if (now - tokenTime > TOKEN_EXPIRY) return false;
const expectedSignature = crypto.createHmac("sha256", env.BETTER_AUTH_SECRET).update(`${token}:${timestamp}`).digest("hex");
return signature === expectedSignature;
}
export async function POST(request: NextRequest) {
const verification = await checkBotId();
if (verification.isBot) {
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
const identifier = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
const { success } = await waitlistRateLimit.limit(identifier);
if (!success) {
return NextResponse.json({ error: "Too many requests. Please try again later." }, { status: 429 });
}
const isValidToken = await validateCSRFToken(request);
if (!isValidToken) {
return NextResponse.json({ error: "Invalid security token" }, { status: 403 });
}
try {
const body = await request.json();
const { email } = waitlistSchema.parse(body);
const existingEmail = await db.select().from(waitlist).where(eq(waitlist.email, email.toLowerCase())).limit(1);
if (existingEmail.length > 0) {
return NextResponse.json({ error: "Email already registered" }, { status: 409 });
}
await db.insert(waitlist).values({
id: nanoid(),
email: email.toLowerCase(),
});
return NextResponse.json({ message: "Successfully joined waitlist!" }, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
const firstError = error.errors[0];
return NextResponse.json({ error: firstError.message }, { status: 400 });
}
console.error("Waitlist signup error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
@@ -1,46 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import crypto from "crypto";
import { env } from "@/env";
const CSRF_TOKEN_NAME = "waitlist-csrf";
const TOKEN_EXPIRY = 60 * 60 * 1000;
const allowedHosts = env.NODE_ENV === "development" ? ["localhost:3000", "127.0.0.1:3000"] : ["opencut.app", "www.opencut.app"];
export async function GET(request: NextRequest) {
const referer = request.headers.get("referer");
const host = request.headers.get("host");
if (referer) {
const refererUrl = new URL(referer);
if (!allowedHosts.some((allowed) => refererUrl.host === allowed || refererUrl.host.endsWith(allowed))) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
} else if (host) {
if (!allowedHosts.some((allowed) => host === allowed || host.endsWith(allowed))) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
} else {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
if (!env.BETTER_AUTH_SECRET) {
throw new Error("BETTER_AUTH_SECRET must be configured");
}
const token = crypto.randomBytes(32).toString("hex");
const timestamp = Date.now();
const signature = crypto.createHmac("sha256", env.BETTER_AUTH_SECRET).update(`${token}:${timestamp}`).digest("hex");
const cookieStore = await cookies();
cookieStore.set(CSRF_TOKEN_NAME, `${token}:${timestamp}:${signature}`, {
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "strict",
maxAge: TOKEN_EXPIRY / 1000,
path: "/",
});
return NextResponse.json({ token });
}
+53
View File
@@ -0,0 +1,53 @@
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import { cn } from "@/utils/ui";
interface BasePageProps {
children: React.ReactNode;
className?: string;
mainClassName?: string;
maxWidth?: "3xl" | "6xl" | "full";
title?: string;
description?: string;
}
export function BasePage({
children,
className = "",
mainClassName = "",
maxWidth = "3xl",
title,
description,
}: BasePageProps) {
const maxWidthClass = {
"3xl": "max-w-3xl",
"6xl": "max-w-6xl",
full: "max-w-full",
}[maxWidth];
return (
<section className={cn("bg-background min-h-screen", className)}>
<Header />
<main
className={cn(
"relative container mx-auto flex flex-col gap-12 px-6 pt-12 pb-24 md:pt-24",
maxWidthClass,
mainClassName,
)}
>
{title && description && (
<div className="flex flex-col gap-8 text-center">
<h1 className="text-5xl font-bold tracking-tight md:text-6xl">
{title}
</h1>
<p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed">
{description}
</p>
</div>
)}
{children}
</main>
<Footer />
</section>
);
}

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