Compare commits

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

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
<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>

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

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

After

Width:  |  Height:  |  Size: 362 B

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

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

-3
View File
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+146
View File
@@ -0,0 +1,146 @@
"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
@@ -0,0 +1,162 @@
"use client";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { memo, Suspense } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ArrowLeft, Loader2 } from "lucide-react";
import { GoogleIcon } from "@/components/icons";
import { useSignUp } from "@/hooks/auth/useSignUp";
const SignUpPage = () => {
const router = useRouter();
const {
name,
setName,
email,
setEmail,
password,
setPassword,
error,
isAnyLoading,
isEmailLoading,
isGoogleLoading,
handleSignUp,
handleGoogleSignUp,
} = useSignUp();
return (
<div className="flex h-screen items-center justify-center relative">
<Button
variant="text"
onClick={() => router.back()}
className="absolute top-6 left-6"
>
<ArrowLeft className="h-5 w-5" /> Back
</Button>
<Card className="w-[400px] shadow-lg border-0">
<CardHeader className="text-center pb-4">
<CardTitle className="text-2xl font-semibold">
Create your account
</CardTitle>
<CardDescription className="text-base">
Get started with your free account today
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<Suspense
fallback={
<div className="text-center">
<Loader2 className="animate-spin" />
</div>
}
>
<div className="flex flex-col space-y-6">
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleGoogleSignUp}
variant="outline"
size="lg"
disabled={isAnyLoading}
>
{isGoogleLoading ? (
<Loader2 className="animate-spin" />
) : (
<GoogleIcon />
)}{" "}
Continue with Google
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator className="w-full" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
type="text"
placeholder="John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="Create a strong password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isAnyLoading}
className="h-11"
/>
</div>
<Button
onClick={handleSignUp}
disabled={isAnyLoading || !name || !email || !password}
className="w-full h-11"
size="lg"
>
{isEmailLoading ? (
<Loader2 className="animate-spin" />
) : (
"Create account"
)}
</Button>
</div>
</div>
<div className="mt-6 text-center text-sm">
Already have an account?{" "}
<Link
href="/login"
className="font-medium text-primary underline-offset-4 hover:underline"
>
Sign in
</Link>
</div>
</Suspense>
</CardContent>
</Card>
</div>
);
};
export default memo(SignUpPage);
+24
View File
@@ -0,0 +1,24 @@
"use client";
import { InputWithBack } from "@/components/ui/input-with-back";
import { useState } from "react";
export default function AnimationPage() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="h-dvh flex">
<div className="bg-panel p-6 w-[20rem]">
<InputWithBack isExpanded={isExpanded} setIsExpanded={setIsExpanded} />
</div>
<div className="p-6 flex items-end justify-end">
<p
onClick={() => setIsExpanded(!isExpanded)}
className="cursor-pointer hover:opacity-75 transition-opacity"
>
{isExpanded ? "Collapse" : "Expand"}
</p>
</div>
</div>
);
}
@@ -0,0 +1,4 @@
import { auth } from "@opencut/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
@@ -0,0 +1,128 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { AwsClient } from "aws4fetch";
import { nanoid } from "nanoid";
import { env } from "@/env";
import { baseRateLimit } from "@/lib/rate-limit";
import { isTranscriptionConfigured } from "@/lib/transcription-utils";
const uploadRequestSchema = z.object({
fileExtension: z.enum(["wav", "mp3", "m4a", "flac"], {
errorMap: () => ({
message: "File extension must be wav, mp3, m4a, or flac",
}),
}),
});
const apiResponseSchema = z.object({
uploadUrl: z.string().url(),
fileName: z.string().min(1),
});
export async function POST(request: NextRequest) {
try {
// Rate limiting
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
const { success } = await baseRateLimit.limit(ip);
if (!success) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
// Check transcription configuration
const transcriptionCheck = isTranscriptionConfigured();
if (!transcriptionCheck.configured) {
console.error(
"Missing environment variables:",
JSON.stringify(transcriptionCheck.missingVars)
);
return NextResponse.json(
{
error: "Transcription not configured",
message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
},
{ status: 503 }
);
}
// Parse and validate request body
const rawBody = await request.json().catch(() => null);
if (!rawBody) {
return NextResponse.json(
{ error: "Invalid JSON in request body" },
{ status: 400 }
);
}
const validationResult = uploadRequestSchema.safeParse(rawBody);
if (!validationResult.success) {
return NextResponse.json(
{
error: "Invalid request parameters",
details: validationResult.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const { fileExtension } = validationResult.data;
// Initialize R2 client
const client = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
});
// Generate unique filename with timestamp
const timestamp = Date.now();
const fileName = `audio/${timestamp}-${nanoid()}.${fileExtension}`;
// Create presigned URL
const url = new URL(
`https://${env.R2_BUCKET_NAME}.${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}`
);
url.searchParams.set("X-Amz-Expires", "3600"); // 1 hour expiry
const signed = await client.sign(new Request(url, { method: "PUT" }), {
aws: { signQuery: true },
});
if (!signed.url) {
throw new Error("Failed to generate presigned URL");
}
// Prepare and validate response
const responseData = {
uploadUrl: signed.url,
fileName,
};
const responseValidation = apiResponseSchema.safeParse(responseData);
if (!responseValidation.success) {
console.error(
"Invalid API response structure:",
responseValidation.error
);
return NextResponse.json(
{ error: "Internal response formatting error" },
{ status: 500 }
);
}
return NextResponse.json(responseValidation.data);
} catch (error) {
console.error("Error generating upload URL:", error);
return NextResponse.json(
{
error: "Failed to generate upload URL",
message:
error instanceof Error
? error.message
: "An unexpected error occurred",
},
{ status: 500 }
);
}
}
+5
View File
@@ -0,0 +1,5 @@
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
return new Response("OK", { status: 200 });
}
+265
View File
@@ -0,0 +1,265 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { env } from "@/env";
import { baseRateLimit } from "@/lib/rate-limit";
const searchParamsSchema = z.object({
q: z.string().max(500, "Query too long").optional(),
type: z.enum(["songs", "effects"]).optional(),
page: z.coerce.number().int().min(1).max(1000).default(1),
page_size: z.coerce.number().int().min(1).max(150).default(20),
sort: z
.enum(["downloads", "rating", "created", "score"])
.default("downloads"),
min_rating: z.coerce.number().min(0).max(5).default(3),
commercial_only: z.coerce.boolean().default(true),
});
const freesoundResultSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
url: z.string().url(),
previews: z
.object({
"preview-hq-mp3": z.string().url(),
"preview-lq-mp3": z.string().url(),
"preview-hq-ogg": z.string().url(),
"preview-lq-ogg": z.string().url(),
})
.optional(),
download: z.string().url().optional(),
duration: z.number(),
filesize: z.number(),
type: z.string(),
channels: z.number(),
bitrate: z.number(),
bitdepth: z.number(),
samplerate: z.number(),
username: z.string(),
tags: z.array(z.string()),
license: z.string(),
created: z.string(),
num_downloads: z.number().optional(),
avg_rating: z.number().optional(),
num_ratings: z.number().optional(),
});
const freesoundResponseSchema = z.object({
count: z.number(),
next: z.string().url().nullable(),
previous: z.string().url().nullable(),
results: z.array(freesoundResultSchema),
});
const transformedResultSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
url: z.string(),
previewUrl: z.string().optional(),
downloadUrl: z.string().optional(),
duration: z.number(),
filesize: z.number(),
type: z.string(),
channels: z.number(),
bitrate: z.number(),
bitdepth: z.number(),
samplerate: z.number(),
username: z.string(),
tags: z.array(z.string()),
license: z.string(),
created: z.string(),
downloads: z.number().optional(),
rating: z.number().optional(),
ratingCount: z.number().optional(),
});
const apiResponseSchema = z.object({
count: z.number(),
next: z.string().nullable(),
previous: z.string().nullable(),
results: z.array(transformedResultSchema),
query: z.string().optional(),
type: z.string(),
page: z.number(),
pageSize: z.number(),
sort: z.string(),
minRating: z.number().optional(),
});
export async function GET(request: NextRequest) {
try {
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
const { success } = await baseRateLimit.limit(ip);
if (!success) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
const { searchParams } = new URL(request.url);
const validationResult = searchParamsSchema.safeParse({
q: searchParams.get("q") || undefined,
type: searchParams.get("type") || undefined,
page: searchParams.get("page") || undefined,
page_size: searchParams.get("page_size") || undefined,
sort: searchParams.get("sort") || undefined,
min_rating: searchParams.get("min_rating") || undefined,
});
if (!validationResult.success) {
return NextResponse.json(
{
error: "Invalid parameters",
details: validationResult.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const {
q: query,
type,
page,
page_size: pageSize,
sort,
min_rating,
commercial_only,
} = validationResult.data;
if (type === "songs") {
return NextResponse.json(
{
error: "Songs are not available yet",
message:
"Song search functionality is coming soon. Try searching for sound effects instead.",
},
{ status: 501 }
);
}
const baseUrl = "https://freesound.org/apiv2/search/text/";
// Use score sorting for search queries, downloads for top sounds
const sortParam = query
? sort === "score"
? "score"
: `${sort}_desc`
: `${sort}_desc`;
const params = new URLSearchParams({
query: query || "",
token: env.FREESOUND_API_KEY,
page: page.toString(),
page_size: pageSize.toString(),
sort: sortParam,
fields:
"id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
});
// Always apply sound effect filters (since we're primarily a sound effects search)
if (type === "effects" || !type) {
params.append("filter", "duration:[* TO 30.0]");
params.append("filter", `avg_rating:[${min_rating} TO *]`);
// Filter by license if commercial_only is true
if (commercial_only) {
params.append(
"filter",
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")'
);
}
params.append(
"filter",
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion"
);
}
const response = await fetch(`${baseUrl}?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
console.error("Freesound API error:", response.status, errorText);
return NextResponse.json(
{ error: "Failed to search sounds" },
{ status: response.status }
);
}
const rawData = await response.json();
const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
if (!freesoundValidation.success) {
console.error(
"Invalid Freesound API response:",
freesoundValidation.error
);
return NextResponse.json(
{ error: "Invalid response from Freesound API" },
{ status: 502 }
);
}
const data = freesoundValidation.data;
const transformedResults = data.results.map((result) => ({
id: result.id,
name: result.name,
description: result.description,
url: result.url,
previewUrl:
result.previews?.["preview-hq-mp3"] ||
result.previews?.["preview-lq-mp3"],
downloadUrl: result.download,
duration: result.duration,
filesize: result.filesize,
type: result.type,
channels: result.channels,
bitrate: result.bitrate,
bitdepth: result.bitdepth,
samplerate: result.samplerate,
username: result.username,
tags: result.tags,
license: result.license,
created: result.created,
downloads: result.num_downloads || 0,
rating: result.avg_rating || 0,
ratingCount: result.num_ratings || 0,
}));
const responseData = {
count: data.count,
next: data.next,
previous: data.previous,
results: transformedResults,
query: query || "",
type: type || "effects",
page,
pageSize,
sort,
minRating: min_rating,
};
const responseValidation = apiResponseSchema.safeParse(responseData);
if (!responseValidation.success) {
console.error(
"Invalid API response structure:",
responseValidation.error
);
return NextResponse.json(
{ error: "Internal response formatting error" },
{ status: 500 }
);
}
return NextResponse.json(responseValidation.data);
} catch (error) {
console.error("Error searching sounds:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
+189
View File
@@ -0,0 +1,189 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { env } from "@/env";
import { baseRateLimit } from "@/lib/rate-limit";
import { isTranscriptionConfigured } from "@/lib/transcription-utils";
const transcribeRequestSchema = z.object({
filename: z.string().min(1, "Filename is required"),
language: z.string().optional().default("auto"),
decryptionKey: z.string().min(1, "Decryption key is required").optional(),
iv: z.string().min(1, "IV is required").optional(),
});
const modalResponseSchema = z.object({
text: z.string(),
segments: z.array(
z.object({
id: z.number(),
seek: z.number(),
start: z.number(),
end: z.number(),
text: z.string(),
tokens: z.array(z.number()),
temperature: z.number(),
avg_logprob: z.number(),
compression_ratio: z.number(),
no_speech_prob: z.number(),
})
),
language: z.string(),
});
const apiResponseSchema = z.object({
text: z.string(),
segments: z.array(
z.object({
id: z.number(),
seek: z.number(),
start: z.number(),
end: z.number(),
text: z.string(),
tokens: z.array(z.number()),
temperature: z.number(),
avg_logprob: z.number(),
compression_ratio: z.number(),
no_speech_prob: z.number(),
})
),
language: z.string(),
});
export async function POST(request: NextRequest) {
try {
// Rate limiting
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
const { success } = await baseRateLimit.limit(ip);
const origin = request.headers.get("origin");
if (!success) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
// Check transcription configuration
const transcriptionCheck = isTranscriptionConfigured();
if (!transcriptionCheck.configured) {
console.error(
"Missing environment variables:",
JSON.stringify(transcriptionCheck.missingVars)
);
return NextResponse.json(
{
error: "Transcription not configured",
message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
},
{ status: 503 }
);
}
// Parse and validate request body
const rawBody = await request.json().catch(() => null);
if (!rawBody) {
return NextResponse.json(
{ error: "Invalid JSON in request body" },
{ status: 400 }
);
}
const validationResult = transcribeRequestSchema.safeParse(rawBody);
if (!validationResult.success) {
return NextResponse.json(
{
error: "Invalid request parameters",
details: validationResult.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const { filename, language, decryptionKey, iv } = validationResult.data;
// Prepare request body for Modal
const modalRequestBody: any = {
filename,
language,
};
// Add encryption parameters if provided (zero-knowledge)
if (decryptionKey && iv) {
modalRequestBody.decryptionKey = decryptionKey;
modalRequestBody.iv = iv;
}
// Call Modal transcription service
const response = await fetch(env.MODAL_TRANSCRIPTION_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(modalRequestBody),
});
if (!response.ok) {
const errorText = await response.text();
console.error("Modal API error:", response.status, errorText);
let errorMessage = "Transcription service unavailable";
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || errorMessage;
} catch {
// Use default message if parsing fails
}
return NextResponse.json(
{
error: errorMessage,
message: "Failed to process transcription request",
},
{ status: response.status >= 500 ? 502 : response.status }
);
}
const rawResult = await response.json();
console.log("Raw Modal response:", JSON.stringify(rawResult, null, 2));
// Validate Modal response
const modalValidation = modalResponseSchema.safeParse(rawResult);
if (!modalValidation.success) {
console.error("Invalid Modal API response:", modalValidation.error);
return NextResponse.json(
{ error: "Invalid response from transcription service" },
{ status: 502 }
);
}
const result = modalValidation.data;
// Prepare and validate API response
const responseData = {
text: result.text,
segments: result.segments,
language: result.language,
};
const responseValidation = apiResponseSchema.safeParse(responseData);
if (!responseValidation.success) {
console.error(
"Invalid API response structure:",
responseValidation.error
);
return NextResponse.json(
{ error: "Internal response formatting error" },
{ status: 500 }
);
}
return NextResponse.json(responseValidation.data);
} catch (error) {
console.error("Transcription API error:", error);
return NextResponse.json(
{
error: "Internal server error",
message: "An unexpected error occurred during transcription",
},
{ status: 500 }
);
}
}
@@ -0,0 +1,83 @@
import { NextRequest, NextResponse } from "next/server";
import { baseRateLimit } from "@/lib/rate-limit";
import { db, exportWaitlist, eq } from "@opencut/db";
import { randomUUID } from "crypto";
import {
exportWaitlistSchema,
exportWaitlistResponseSchema,
} from "@/lib/schemas/waitlist";
const requestSchema = exportWaitlistSchema;
const responseSchema = exportWaitlistResponseSchema;
export async function POST(request: NextRequest) {
try {
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
const { success } = await baseRateLimit.limit(ip);
if (!success) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
const body = await request.json().catch(() => null);
if (!body) {
return NextResponse.json(
{ error: "Invalid JSON in request body" },
{ status: 400 }
);
}
const parsed = requestSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{
error: "Invalid request parameters",
details: parsed.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const { email } = parsed.data;
const existing = await db
.select({ id: exportWaitlist.id })
.from(exportWaitlist)
.where(eq(exportWaitlist.email, email))
.limit(1);
if (existing.length > 0) {
const responseData = { success: true, alreadySubscribed: true } as const;
const validated = responseSchema.safeParse(responseData);
if (!validated.success) {
return NextResponse.json(
{ error: "Internal response formatting error" },
{ status: 500 }
);
}
return NextResponse.json(validated.data);
}
await db.insert(exportWaitlist).values({
id: randomUUID(),
email,
createdAt: new Date(),
updatedAt: new Date(),
});
const responseData = { success: true } as const;
const validated = responseSchema.safeParse(responseData);
if (!validated.success) {
return NextResponse.json(
{ error: "Internal response formatting error" },
{ status: 500 }
);
}
return NextResponse.json(validated.data);
} catch (error) {
console.error("Waitlist API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
+144
View File
@@ -0,0 +1,144 @@
import { Header } from "@/components/header";
import Prose from "@/components/ui/prose";
import { Separator } from "@/components/ui/separator";
import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog-query";
import { Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const slug = (await params).slug;
const data = await getSinglePost(slug);
if (!data || !data.post) return {};
return {
title: data.post.title,
description: data.post.description,
twitter: {
title: `${data.post.title}`,
description: `${data.post.description}`,
card: "summary_large_image",
images: [
{
url: data.post.coverImage,
width: "1200",
height: "630",
alt: data.post.title,
},
],
},
openGraph: {
type: "article",
images: [
{
url: data.post.coverImage,
width: "1200",
height: "630",
alt: data.post.title,
},
],
title: data.post.title,
description: data.post.description,
publishedTime: new Date(data.post.publishedAt).toISOString(),
authors: [
...data.post.authors.map((author: { name: string }) => author.name),
],
},
};
}
export async function generateStaticParams() {
const data = await getPosts();
if (!data || !data.posts.length) return [];
return data.posts.map((post) => ({
slug: post.slug,
}));
}
async function Page({ params }: PageProps) {
const slug = (await params).slug;
const data = await getSinglePost(slug);
if (!data || !data.post) return notFound();
const html = await processHtmlContent(data.post.content);
const formattedDate = new Date(data.post.publishedAt).toLocaleDateString(
"en-US",
{
day: "numeric",
month: "long",
year: "numeric",
}
);
return (
<div className="min-h-screen bg-background">
<Header />
<main className="relative">
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
</div>
<div className="relative container max-w-3xl mx-auto px-4 py-16">
<div className="text-center mb-6">
{data.post.coverImage && (
<div className="relative aspect-video rounded-lg overflow-hidden mb-6">
<Image
src={data.post.coverImage}
alt={data.post.title}
loading="eager"
fill
className="object-cover rounded-lg"
/>
</div>
)}
<div className="flex items-center justify-center mb-6">
<time dateTime={data.post.publishedAt.toString()}>
{formattedDate}
</time>
</div>
<h1 className="text-5xl md:text-4xl font-bold tracking-tight mb-6">
{data.post.title}
</h1>
<div className="flex items-center justify-center gap-2">
{data.post.authors[0] && (
<>
<Image
src={data.post.authors[0].image}
alt={data.post.authors[0].name}
width={36}
height={36}
loading="eager"
className="aspect-square shrink-0 size-8 rounded-full"
/>
<p className="text-muted-foreground">
{data.post.authors[0].name}
</p>
</>
)}
</div>
</div>
<Separator />
<section className="mt-14">
<Prose html={html} />
</section>
</div>
</main>
</div>
);
}
export default Page;

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