commit efbebd13b89f5adfc1110f300695ecc3f8369ec3 Author: Maze Winther Date: Wed Nov 26 08:47:03 2025 +0100 refactor not done diff --git a/.cursor/rules/comments.mdc b/.cursor/rules/comments.mdc new file mode 100644 index 00000000..e732e859 --- /dev/null +++ b/.cursor/rules/comments.mdc @@ -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. \ No newline at end of file diff --git a/.cursor/rules/handling-uncertainty.mdc b/.cursor/rules/handling-uncertainty.mdc new file mode 100644 index 00000000..c6bae730 --- /dev/null +++ b/.cursor/rules/handling-uncertainty.mdc @@ -0,0 +1,21 @@ +--- +alwaysApply: true +--- + +# Handling Uncertainty + +## Principle +If you can't confidently respond due to missing context, data access, or ambiguity (multiple interpretations), report it instead of guessing. Seek clarification to avoid errors. + +Apply when: query lacks details, no access to info/tools, or unclear intent. + +## How to Report +1. **Description**: Why uncertain and what you need. +2. **Questions**: 1-3 targeted ones. +3. **Assumptions** (opt.): State if proceeding; omit otherwise. + +Direct and concise. + +**Assumptions**: None. + +Builds transparency. \ No newline at end of file diff --git a/.cursor/rules/separation-of-concerns.mdc b/.cursor/rules/separation-of-concerns.mdc new file mode 100644 index 00000000..0dd97118 --- /dev/null +++ b/.cursor/rules/separation-of-concerns.mdc @@ -0,0 +1,52 @@ +--- +alwaysApply: true +--- + +# Separation of Concerns + +## Core Principle + +Each file should have one single purpose/responsibility. Related functionality should be grouped together, unrelated functionality should be separated. + +## Good Separation + +- One file per major concern (auth, validation, data transformation) +- Group related utilities together +- Extract shared logic into dedicated files +- Keep API routes focused on their specific endpoint logic + +Examples: + +```javascript +// ✅ Good: Each file has clear responsibility +/lib/rate-limit.ts // Rate limiting utilities +/lib/validation.ts // Input validation schemas +/lib/freesound-api.ts // External API integration +/api/sounds/search/route.ts // Route handler only +``` + +## Bad Mixing of Concerns + +Avoid cramming multiple responsibilities into one file: + +```javascript +// ❌ Bad: Route file doing everything +/api/sounds/search/route.ts +- Rate limiting logic +- Validation schemas +- API transformation +- External API calls +- Response formatting +- Error handling utilities +``` + +## When to Separate + +- File is getting long (>500 lines) +- Multiple distinct responsibilities in one file +- Logic could be reused elsewhere +- Complex utilities that distract from main purpose + +## Rule + +One file, one responsibility. Extract shared concerns into focused utility files \ No newline at end of file diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc new file mode 100644 index 00000000..c8208f06 --- /dev/null +++ b/.cursor/rules/ultracite.mdc @@ -0,0 +1,105 @@ +--- +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 use both `children` and `dangerouslySetInnerHTML` props on the same element. +- Don't insert comments as text nodes. +- Use `<>...` instead of `...`. + +### 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` 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 `` elements in Next.js projects. +- Don't use `` 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); +} +``` diff --git a/.cursor/rules/writing-scannable-code.mdc b/.cursor/rules/writing-scannable-code.mdc new file mode 100644 index 00000000..97ed87e9 --- /dev/null +++ b/.cursor/rules/writing-scannable-code.mdc @@ -0,0 +1,53 @@ +--- +alwaysApply: true +--- + +# Scannable Code Guidelines/Separating Concerns. + +## Core Principle + +Code should be scannable through proper abstraction, not comments. Use variables and helper functions to make intent clear at a glance. + +## Good Scannable Code + +- Extract complex logic into well-named variables +- Create helper functions for multi-step operations +- Use descriptive names that explain intent + +Examples: + +```javascript +// ✅ Scannable: Intent is clear from variable names +const isValidUser = user.isActive && user.hasPermissions; +const shouldProcessPayment = amount > 0 && !order.isPaid; + +// ✅ Scannable: Complex logic extracted to helper +const searchParams = buildFreesoundSearchParams({ query, filters, pagination }); +const transformedResults = transformFreesoundResults({ rawResults }); +``` + +## Bad Unscannable Code + +Avoid: + +```javascript +// ❌ Hard to scan: What does this condition mean? +if (type === "effects" || !type) { + params.append("filter", "duration:[* TO 30.0]"); + params.append("filter", `avg_rating:[${min_rating} TO *]`); + if (commercial_only) { + params.append("filter", 'license:("Attribution" OR "Creative Commons 0")'); + } +} + +// ❌ Hard to scan: Complex ternary +const sortParam = query + ? sort === "score" + ? "score" + : `${sort}_desc` + : `${sort}_desc`; +``` + +## Rule + +Make code scannable by extracting intent into variables and helper functions. If you need to think about what code does, extract it. The reader should understand the flow without diving into implementation details. \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..b6d67872 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,89 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at our discord server. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..f1b425e4 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -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:opencut@localhost:5432/opencut" + + # Generate a secure secret for Better Auth + BETTER_AUTH_SECRET="your-generated-secret-here" + NEXT_PUBLIC_SITE_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! diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..c9684d1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,70 @@ +name: Bug report +description: Create a report to help us improve +title: '[BUG] ' +labels: bug +body: + - type: input + id: Platform + attributes: + label: Platform + description: Please enter the platform on which you encountered the bug. + placeholder: e.g. Windows 11, Ubuntu 14.04 + validations: + required: true + - type: input + id: Browser + attributes: + label: Browser + description: Please enter the browser on which you encountered the bug. + placeholder: e.g. Chrome 137, Firefox 137, Safari 17 + validations: + required: true + - type: textarea + id: current-behavior + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false + - type: dropdown + id: recurrence-probability + attributes: + label: Recurrence Probability + description: How often does this bug occur? + options: + - Always + - Usually + - Sometimes + - Seldom + default: 0 + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..49a00eb6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,42 @@ +name: Feature request +description: Suggest an idea for OpenCut +title: '[FEATURE] ' +labels: enhancement +body: + - type: markdown + attributes: + value: Please make sure that no duplicated issues has already been delivered. + - type: textarea + id: problem + attributes: + label: Problem + placeholder: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + id: solution + attributes: + label: Solution + placeholder: Describe the solution you'd like. + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternative + attributes: + label: Alternative + placeholder: Describe alternatives you've considered. + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..f6d94c5e --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,28 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | + +## Reporting a Vulnerability + +We take security vulnerabilities seriously. If you discover a security vulnerability within OpenCut, please send an email to security@opencut.app. All security vulnerabilities will be promptly addressed. + +Please do not report security vulnerabilities through public GitHub issues. + +### What to include in your report + +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Any suggested fixes + +### Response timeline + +- We will acknowledge receipt within 48 hours +- We will provide a detailed response within 5 business days +- We will keep you updated on our progress + +Thank you for helping keep OpenCut secure! \ No newline at end of file diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 00000000..892a0f7a --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,23 @@ +# Getting Help + +Thanks for using OpenCut! If you need help, here are your options: + +## Documentation +- Check our [README](../README.md) for basic setup instructions +- Review the [Contributing Guidelines](CONTRIBUTING.md) for development setup + +## Issues +- **Bug reports**: Use the bug report template +- **Feature requests**: Use the feature request template +- **Questions**: Use GitHub Discussions for general questions + +## Community +- Join our discussions on GitHub +- Follow the [Code of Conduct](CODE_OF_CONDUCT.md) + +## Response Times +- Issues are typically triaged within 2-3 business days +- Feature requests may take longer to evaluate +- Security issues are handled with priority + +We appreciate your patience and contributions to making OpenCut better! \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..5d6e71f2 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,331 @@ +--- +applyTo: "**/*.{ts,tsx,js,jsx}" +--- + +# Project Context +Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. + +## Key Principles +- Zero configuration required +- Subsecond performance +- Maximum type safety +- AI-friendly code generation + +## Before Writing Code +1. Analyze existing patterns in the codebase +2. Consider edge cases and error scenarios +3. Follow the rules below strictly +4. Validate accessibility requirements + +## Rules + +### Accessibility (a11y) +- Don't use `accessKey` attribute on any HTML element. +- Don't set `aria-hidden="true"` on focusable elements. +- Don't add ARIA roles, states, and properties to elements that don't support them. +- Don't use distracting elements like `` or ``. +- Only use the `scope` prop on `` elements. +- Don't assign non-interactive ARIA roles to interactive HTML elements. +- Make sure label elements have text content and are associated with an input. +- Don't assign interactive ARIA roles to non-interactive HTML elements. +- Don't assign `tabIndex` to non-interactive HTML elements. +- Don't use positive integers for `tabIndex` property. +- Don't include "image", "picture", or "photo" in img alt prop. +- Don't use explicit role property that's the same as the implicit/default role. +- Make static elements with click handlers use a valid role attribute. +- Always include a `title` element for SVG elements. +- Give all elements requiring alt text meaningful information for screen readers. +- Make sure anchors have content that's accessible to screen readers. +- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. +- Include all required ARIA attributes for elements with ARIA roles. +- Make sure ARIA properties are valid for the element's supported roles. +- Always include a `type` attribute for button elements. +- Make elements with interactive roles and handlers focusable. +- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). +- Always include a `lang` attribute on the html element. +- Always include a `title` attribute for iframe elements. +- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. +- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. +- Include caption tracks for audio and video elements. +- Use semantic elements instead of role attributes in JSX. +- Make sure all anchors are valid and navigable. +- Ensure all ARIA properties (`aria-*`) are valid. +- Use valid, non-abstract ARIA roles for elements with ARIA roles. +- Use valid ARIA state and property values. +- Use valid values for the `autocomplete` attribute on input elements. +- Use correct ISO language/country codes for the `lang` attribute. + +### Code Complexity and Quality +- Don't use consecutive spaces in regular expression literals. +- Don't use the `arguments` object. +- Don't use primitive type aliases or misleading types. +- Don't use the comma operator. +- Don't use empty type parameters in type aliases and interfaces. +- Don't write functions that exceed a given Cognitive Complexity score. +- Don't nest describe() blocks too deeply in test files. +- Don't use unnecessary boolean casts. +- Don't use unnecessary callbacks with flatMap. +- Use for...of statements instead of Array.forEach. +- Don't create classes that only have static members (like a static namespace). +- Don't use this and super in static contexts. +- Don't use unnecessary catch clauses. +- Don't use unnecessary constructors. +- Don't use unnecessary continue statements. +- Don't export empty modules that don't change anything. +- Don't use unnecessary escape sequences in regular expression literals. +- Don't use unnecessary fragments. +- Don't use unnecessary labels. +- Don't use unnecessary nested block statements. +- Don't rename imports, exports, and destructured assignments to the same name. +- Don't use unnecessary string or template literal concatenation. +- Don't use String.raw in template literals when there are no escape sequences. +- Don't use useless case statements in switch statements. +- Don't use ternary operators when simpler alternatives exist. +- Don't use useless `this` aliasing. +- Don't use any or unknown as type constraints. +- Don't initialize variables to undefined. +- Don't use the void operators (they're not familiar). +- Use arrow functions instead of function expressions. +- Use Date.now() to get milliseconds since the Unix Epoch. +- Use .flatMap() instead of map().flat() when possible. +- Use literal property access instead of computed property access. +- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. +- Use concise optional chaining instead of chained logical expressions. +- Use regular expression literals instead of the RegExp constructor when possible. +- Don't use number literal object member names that aren't base 10 or use underscore separators. +- Remove redundant terms from logical expressions. +- Use while loops instead of for loops when you don't need initializer and update expressions. +- Don't pass children as props. +- Don't reassign const variables. +- Don't use constant expressions in conditions. +- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. +- Don't return a value from a constructor. +- Don't use empty character classes in regular expression literals. +- Don't use empty destructuring patterns. +- Don't call global object properties as functions. +- Don't declare functions and vars that are accessible outside their block. +- Make sure builtins are correctly instantiated. +- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. +- Don't use variables and function parameters before they're declared. +- Don't use 8 and 9 escape sequences in string literals. +- Don't use literal numbers that lose precision. + +### React and JSX Best Practices +- Don't use the return value of React.render. +- Make sure all dependencies are correctly specified in React hooks. +- Make sure all React hooks are called from the top level of component functions. +- Don't forget key props in iterators and collection literals. +- Don't destructure props inside JSX components in Solid projects. +- Don't define React components inside other components. +- Don't use event handlers on non-interactive elements. +- Don't assign to React component props. +- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. +- Don't use dangerous JSX props. +- Don't use Array index in keys. +- Don't insert comments as text nodes. +- Don't assign JSX properties multiple times. +- Don't add extra closing tags for components without children. +- Use `<>...` instead of `...`. +- Watch out for possible "wrong" semicolons inside JSX elements. + +### Correctness and Safety +- Don't assign a value to itself. +- Don't return a value from a setter. +- Don't compare expressions that modify string case with non-compliant values. +- Don't use lexical declarations in switch clauses. +- Don't use variables that haven't been declared in the document. +- Don't write unreachable code. +- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. +- Don't use control flow statements in finally blocks. +- Don't use optional chaining where undefined values aren't allowed. +- Don't have unused function parameters. +- Don't have unused imports. +- Don't have unused labels. +- Don't have unused private class members. +- Don't have unused variables. +- Make sure void (self-closing) elements don't have children. +- Don't return a value from a function with the return type 'void' +- Use isNaN() when checking for NaN. +- Make sure "for" loop update clauses move the counter in the right direction. +- Make sure typeof expressions are compared to valid values. +- Make sure generator functions contain yield. +- Don't use await inside loops. +- Don't use bitwise operators. +- Don't use expressions where the operation doesn't change the value. +- Make sure Promise-like statements are handled appropriately. +- Don't use __dirname and __filename in the global scope. +- Prevent import cycles. +- Don't use configured elements. +- Don't hardcode sensitive data like API keys and tokens. +- Don't let variable declarations shadow variables from outer scopes. +- Don't use the TypeScript directive @ts-ignore. +- Prevent duplicate polyfills from Polyfill.io. +- Don't use useless backreferences in regular expressions that always match empty strings. +- Don't use unnecessary escapes in string literals. +- Don't use useless undefined. +- Make sure getters and setters for the same property are next to each other in class and object definitions. +- Make sure object literals are declared consistently (defaults to explicit definitions). +- Use static Response methods instead of new Response() constructor when possible. +- Make sure switch-case statements are exhaustive. +- Make sure the `preconnect` attribute is used when using Google Fonts. +- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. +- Make sure iterable callbacks return consistent values. +- Use `with { type: "json" }` for JSON module imports. +- Use numeric separators in numeric literals. +- Use object spread instead of `Object.assign()` when constructing new objects. +- Always use the radix argument when using `parseInt()`. +- Make sure JSDoc comment lines start with a single asterisk, except for the first one. +- Include a description parameter for `Symbol()`. +- Don't use spread (`...`) syntax on accumulators. +- Don't use the `delete` operator. +- Don't access namespace imports dynamically. +- Don't use namespace imports. +- Declare regex literals at the top level. +- Don't use `target="_blank"` without `rel="noopener"`. + +### TypeScript Best Practices +- Don't use TypeScript enums. +- Don't export imported variables. +- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. +- Don't use TypeScript namespaces. +- Don't use non-null assertions with the `!` postfix operator. +- Don't use parameter properties in class constructors. +- Don't use user-defined types. +- Use `as const` instead of literal types and type annotations. +- Use either `T[]` or `Array` consistently. +- Initialize each enum member value explicitly. +- Use `export type` for types. +- Use `import type` for types. +- Make sure all enum members are literal values. +- Don't use TypeScript const enum. +- Don't declare empty interfaces. +- Don't let variables evolve into any type through reassignments. +- Don't use the any type. +- Don't misuse the non-null assertion operator (!) in TypeScript files. +- Don't use implicit any type on variable declarations. +- Don't merge interfaces and classes unsafely. +- Don't use overload signatures that aren't next to each other. +- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. + +### Style and Consistency +- Don't use global `eval()`. +- Don't use callbacks in asynchronous tests and hooks. +- Don't use negation in `if` statements that have `else` clauses. +- Don't use nested ternary expressions. +- Don't reassign function parameters. +- This rule lets you specify global variable names you don't want to use in your application. +- Don't use specified modules when loaded by import or require. +- Don't use constants whose value is the upper-case version of their name. +- Use `String.slice()` instead of `String.substr()` and `String.substring()`. +- Don't use template literals if you don't need interpolation or special-character handling. +- Don't use `else` blocks when the `if` block breaks early. +- Don't use yoda expressions. +- Don't use Array constructors. +- Use `at()` instead of integer index access. +- Follow curly brace conventions. +- Use `else if` instead of nested `if` statements in `else` clauses. +- Use single `if` statements instead of nested `if` clauses. +- Use `new` for all builtins except `String`, `Number`, and `Boolean`. +- Use consistent accessibility modifiers on class properties and methods. +- Use `const` declarations for variables that are only assigned once. +- Put default function parameters and optional function parameters last. +- Include a `default` clause in switch statements. +- Use the `**` operator instead of `Math.pow`. +- Use `for-of` loops when you need the index to extract an item from the iterated array. +- Use `node:assert/strict` over `node:assert`. +- Use the `node:` protocol for Node.js builtin modules. +- Use Number properties instead of global ones. +- Use assignment operator shorthand where possible. +- Use function types instead of object types with call signatures. +- Use template literals over string concatenation. +- Use `new` when throwing an error. +- Don't throw non-Error values. +- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. +- Use standard constants instead of approximated literals. +- Don't assign values in expressions. +- Don't use async functions as Promise executors. +- Don't reassign exceptions in catch clauses. +- Don't reassign class members. +- Don't compare against -0. +- Don't use labeled statements that aren't loops. +- Don't use void type outside of generic or return types. +- Don't use console. +- Don't use control characters and escape sequences that match control characters in regular expression literals. +- Don't use debugger. +- Don't assign directly to document.cookie. +- Use `===` and `!==`. +- Don't use duplicate case labels. +- Don't use duplicate class members. +- Don't use duplicate conditions in if-else-if chains. +- Don't use two keys with the same name inside objects. +- Don't use duplicate function parameter names. +- Don't have duplicate hooks in describe blocks. +- Don't use empty block statements and static blocks. +- Don't let switch clauses fall through. +- Don't reassign function declarations. +- Don't allow assignments to native objects and read-only global variables. +- Use Number.isFinite instead of global isFinite. +- Use Number.isNaN instead of global isNaN. +- Don't assign to imported bindings. +- Don't use irregular whitespace characters. +- Don't use labels that share a name with a variable. +- Don't use characters made with multiple code points in character class syntax. +- Make sure to use new and constructor properly. +- Don't use shorthand assign when the variable appears on both sides. +- Don't use octal escape sequences in string literals. +- Don't use Object.prototype builtins directly. +- Don't redeclare variables, functions, classes, and types in the same scope. +- Don't have redundant "use strict". +- Don't compare things where both sides are exactly the same. +- Don't let identifiers shadow restricted names. +- Don't use sparse arrays (arrays with holes). +- Don't use template literal placeholder syntax in regular strings. +- Don't use the then property. +- Don't use unsafe negation. +- Don't use var. +- Don't use with statements in non-strict contexts. +- Make sure async functions actually use await. +- Make sure default clauses in switch statements come last. +- Make sure to pass a message value when creating a built-in error. +- Make sure get methods always return a value. +- Use a recommended display strategy with Google Fonts. +- Make sure for-in loops include an if statement. +- Use Array.isArray() instead of instanceof Array. +- Make sure to use the digits argument with Number#toFixed(). +- Make sure to use the "use strict" directive in script files. + +### Next.js Specific Rules +- Don't use `` elements in Next.js projects. +- Don't use `` elements in Next.js projects. +- Don't import next/document outside of pages/_document.jsx in Next.js projects. +- Don't use the next/head module in pages/_document.js on Next.js projects. + +### Testing Best Practices +- Don't use export or module.exports in test files. +- Don't use focused tests. +- Make sure the assertion function, like expect, is placed inside an it() function call. +- Don't use disabled tests. + +## Common Tasks +- `npx ultracite init` - Initialize Ultracite in your project +- `npx ultracite format` - Format and fix code automatically +- `npx ultracite lint` - Check for issues without fixing + +## Example: Error Handling +```typescript +// ✅ Good: Comprehensive error handling +try { + const result = await fetchData(); + return { success: true, data: result }; +} catch (error) { + console.error('API call failed:', error); + return { success: false, error: error.message }; +} + +// ❌ Bad: Swallowing errors +try { + return await fetchData(); +} catch (e) { + console.log(e); +} +``` \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..b7686c4b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,49 @@ +## Description + +Please include a summary of the changes and the related issue. Please also include relevant motivation and context. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update +- [ ] Performance improvement +- [ ] Code refactoring +- [ ] Tests + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +**Test Configuration**: +* Node version: +* Browser (if applicable): +* Operating System: + +## Screenshots (if applicable) + +Add screenshots to help explain your changes. + +## Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have added screenshots if ui has been changed +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules + +## Additional context + +Add any other context about the pull request here. diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml new file mode 100644 index 00000000..d3e4dcb7 --- /dev/null +++ b/.github/workflows/bun-ci.yml @@ -0,0 +1,57 @@ +name: Bun CI + +concurrency: + group: bun-ci-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths-ignore: + - "*.md" + pull_request: + branches: [main] + paths-ignore: + - "*.md" + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + env: + DATABASE_URL: "postgresql://opencut:opencut@localhost:5432/opencut" + BETTER_AUTH_SECRET: "supersecret" + NEXT_PUBLIC_SITE_URL: "http://localhost:3000" + UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url" + UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 + with: + bun-version: 1.2.18 + + - name: Cache Bun modules + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-1.2.18-${{ hashFiles('apps/web/bun.lock') }} + + - name: Install dependencies + working-directory: apps/web + run: bun install + + - name: Build + working-directory: apps/web + run: bun run build + + - name: Run tests + working-directory: apps/web + run: echo "No tests implemented yet" + continue-on-error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..64368c76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# asdf version management +.tool-versions + +node_modules +.cursorignore +.turbo + +.env* +!*.env.example + +# cursor +bun.lockb \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..c5465713 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +install-strategy="nested" +node-linker=isolated \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..8b0bc4ef --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..501babd0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "typescript.tsdk": "node_modules/typescript/lib", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "emmet.showExpandedAbbreviation": "never", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f9a333a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +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: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..d0421dd5 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ + + + + + +
+ OpenCut Logo + +

OpenCut

+

A free, open-source video editor for web, desktop, and mobile.

+
+ +## Why? + +- **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 + +## Features + +- Timeline-based editing +- Multi-track support +- Real-time preview +- No watermarks or subscriptions +- Analytics provided by [Databuddy](https://www.databuddy.cc?utm_source=opencut), 100% Anonymized & Non-invasive. +- Blog powered by [Marble](https://marblecms.com?utm_source=opencut), Headless CMS. + +## Project Structure + +- `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 + +## Getting Started + +### Prerequisites + +Before you begin, ensure you have the following installed on your system: + +- [Node.js](https://nodejs.org/en/) (v18 or later) +- [Bun](https://bun.sh/docs/installation) + (for `npm` alternative) +- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) + +> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to run the frontend or want to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go! + +### Setup + +1. Fork the repository +2. Clone your fork locally +3. Navigate to the web app directory: `cd apps/web` +4. Copy `.env.example` to `.env.local`: + + ```bash + # Unix/Linux/Mac + cp .env.example .env.local + + # Windows Command Prompt + copy .env.example .env.local + + # Windows PowerShell + Copy-Item .env.example .env.local + ``` + +5. Install dependencies: `bun install` +6. Start the development server: `bun dev` + +## Development Setup + +### Local Development + +1. Start the database and Redis services: + + ```bash + # From project root + docker-compose up -d + ``` + +2. Navigate to the web app directory: + + ```bash + cd apps/web + ``` + +3. Copy `.env.example` to `.env.local`: + + ```bash + # Unix/Linux/Mac + cp .env.example .env.local + + # Windows Command Prompt + copy .env.example .env.local + + # Windows PowerShell + Copy-Item .env.example .env.local + ``` + +4. Configure required environment variables in `.env.local`: + + **Required Variables:** + + ```bash + # Database (matches docker-compose.yaml) + DATABASE_URL="postgresql://opencut:opencut@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 + +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 + +Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software. + + + Vercel OSS Program + + + + Powered by fal.ai + + +--- + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FOpenCut-app%2FOpenCut&project-name=opencut&repository-name=opencut) + +## License + +[MIT LICENSE](LICENSE) + +--- + +![Star History Chart](https://api.star-history.com/svg?repos=opencut-app/opencut&type=Date) diff --git a/apps/tools/.env.example b/apps/tools/.env.example new file mode 100644 index 00000000..61fa2fdb --- /dev/null +++ b/apps/tools/.env.example @@ -0,0 +1,20 @@ +# Environment variables example +# Copy this file to .env.local and update the values as needed + +# Node +NODE_ENV=development + +# Public +NEXT_PUBLIC_SITE_URL=http://localhost:3001 + +# Server +DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut" +BETTER_AUTH_SECRET=your_better_auth_secret + +UPSTASH_REDIS_REST_URL=http://localhost:8079 +UPSTASH_REDIS_REST_TOKEN=example_token_here + +CLOUDFLARE_ACCOUNT_ID=your_account_id_here +R2_ACCESS_KEY_ID=your_access_key_here +R2_SECRET_ACCESS_KEY=your_secret_key_here +R2_BUCKET_NAME=opencut-tools \ No newline at end of file diff --git a/apps/tools/.gitignore b/apps/tools/.gitignore new file mode 100644 index 00000000..44c63a77 --- /dev/null +++ b/apps/tools/.gitignore @@ -0,0 +1,6 @@ +# Turborepo +.turbo + +# Env vars +.env* +!.env.example \ No newline at end of file diff --git a/apps/tools/README.md b/apps/tools/README.md new file mode 100644 index 00000000..c0dfd9ac --- /dev/null +++ b/apps/tools/README.md @@ -0,0 +1,59 @@ +# OpenCut Tools + +## Why this exists + +I needed to replace a color in an image the other day. + +Spent 20 minutes trying to find a tool that wasn't shit. + +**Site 1:** Can't paste images. Have to download my clipboard image, upload it, then delete it. Immediately close tab. + +**Site 2:** Can't paste images either. Close. + +**Site 3:** Finally! Paste support! Oh wait—massive "UPGRADE TO PRO NOW" popup blocks the entire screen. Close popup. UI has 500 buttons everywhere. Okay whatever, let's try the color picker... **NO EYEDROPPER**. A color replacement tool with no way to select colors from the image. The most obvious feature. Not there. + +Now I have to open ANOTHER site just to extract the hex code from my image. Google "extract color from image". Find a site. Upload image again. Get the color. Copy hex code. + +Go back to the replacement site. Paste the hex. Click replace. + +**It didn't even work.** + +Started over. Sat there for 10 minutes and it eventually worked. + +--- + +## The no-bullshit version + +Every image/video tool on the internet has the same problems: + +- Can't paste images +- Paywalls for basic features +- Aggressive popups and ads +- Bloated UIs +- Missing obvious features (eyedroppers, copy result, etc) +- Can't batch process +- Tracking your every move +- Login to use the tool + +**OpenCut Tools is different:** + +- ✅ Paste images directly +- ✅ Every tool has the features you'd expect +- ✅ Copy results +- ✅ No login required for free tier +- ✅ Minimal, clean UI +- ✅ Privacy-first (we don't want your data) +- ✅ Batch processing +- ✅ No ads, no popups, no bullshit + +Free for most users. Premium ($8/month) for power features like batch processing and advanced tools. + +--- + +## The mission + +Same as OpenCut: build tools that don't suck and actually respect your privacy. + +No VC money. No dark patterns. No selling your data. + +Just tools that work the way they should have from the beginning. diff --git a/apps/tools/components.json b/apps/tools/components.json new file mode 100644 index 00000000..3289f237 --- /dev/null +++ b/apps/tools/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/apps/tools/next-env.d.ts b/apps/tools/next-env.d.ts new file mode 100644 index 00000000..830fb594 --- /dev/null +++ b/apps/tools/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/tools/next.config.ts b/apps/tools/next.config.ts new file mode 100644 index 00000000..4c92fbd2 --- /dev/null +++ b/apps/tools/next.config.ts @@ -0,0 +1,13 @@ +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", +}; + +export default withBotId(nextConfig); diff --git a/apps/tools/package.json b/apps/tools/package.json new file mode 100644 index 00000000..dc7f2f19 --- /dev/null +++ b/apps/tools/package.json @@ -0,0 +1,75 @@ +{ + "name": "@opencut/tools", + "version": "0.1.0", + "private": true, + "packageManager": "bun@1.2.18", + "scripts": { + "dev": "next dev --turbopack --port 3001", + "build": "next build", + "start": "next start --port 3001", + "lint": "biome check src/", + "lint:fix": "biome check src/ --write", + "format": "biome format src/ --write" + }, + "dependencies": { + "@hookform/resolvers": "^3.9.1", + "@opencut/env": "workspace:*", + "@radix-ui/react-separator": "^1.1.7", + "@upstash/ratelimit": "^2.0.6", + "@upstash/redis": "^1.35.4", + "aws4fetch": "^1.0.20", + "better-auth": "^1.2.7", + "botid": "^1.4.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "dotenv": "^16.5.0", + "embla-carousel-react": "^8.5.1", + "input-otp": "^1.4.1", + "lucide-react": "^0.468.0", + "motion": "^12.18.1", + "nanoid": "^5.1.5", + "next": "^15.5.3", + "next-themes": "^0.4.4", + "pg": "^8.16.2", + "radix-ui": "^1.4.2", + "mediabunny": "^1.9.3", + "react": "^18.2.0", + "react-day-picker": "^8.10.1", + "react-dom": "^18.2.0", + "react-hook-form": "^7.54.0", + "react-icons": "^5.4.0", + "react-markdown": "^10.1.0", + "react-phone-number-input": "^3.4.11", + "react-resizable-panels": "^2.1.7", + "recharts": "^2.14.1", + "rehype-autolink-headings": "^7.1.0", + "rehype-parse": "^9.0.1", + "rehype-sanitize": "^6.0.0", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.1", + "sonner": "^1.7.1", + "tailwind-merge": "^2.5.5", + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.1", + "zod": "^3.25.67", + "zustand": "^5.0.2" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.11", + "@tailwindcss/typography": "^0.5.16", + "@types/bun": "latest", + "@types/node": "^24.2.1", + "@types/pg": "^8.15.4", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "cross-env": "^7.0.3", + "drizzle-kit": "^0.31.4", + "postcss": "^8", + "prettier": "^3.6.2", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.1.11", + "tsx": "^4.7.1", + "typescript": "^5.8.3" + } +} \ No newline at end of file diff --git a/apps/tools/postcss.config.mjs b/apps/tools/postcss.config.mjs new file mode 100644 index 00000000..79bcf135 --- /dev/null +++ b/apps/tools/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/apps/tools/public/favicon.ico b/apps/tools/public/favicon.ico new file mode 100644 index 00000000..9b2cba8f Binary files /dev/null and b/apps/tools/public/favicon.ico differ diff --git a/apps/tools/src/app/api/auth/[...all]/route.ts b/apps/tools/src/app/api/auth/[...all]/route.ts new file mode 100644 index 00000000..5deef204 --- /dev/null +++ b/apps/tools/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@opencut/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { POST, GET } = toNextJsHandler(auth); diff --git a/apps/tools/src/app/api/get-upload-url/route.ts b/apps/tools/src/app/api/get-upload-url/route.ts new file mode 100644 index 00000000..48e0dfc4 --- /dev/null +++ b/apps/tools/src/app/api/get-upload-url/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { AwsClient } from "aws4fetch"; +import { nanoid } from "nanoid"; +import { env } from "@/env"; +import { checkRateLimit } 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 { + const { limited } = await checkRateLimit({ request }); + if (limited) { + return NextResponse.json({ error: "Too many requests" }, { status: 429 }); + } + + 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 } + ); + } + + 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; + + const client = new AwsClient({ + accessKeyId: env.R2_ACCESS_KEY_ID, + secretAccessKey: env.R2_SECRET_ACCESS_KEY, + }); + + const timestamp = Date.now(); + const fileName = `audio/${timestamp}-${nanoid()}.${fileExtension}`; + + 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"); + } + + 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 } + ); + } +} \ No newline at end of file diff --git a/apps/tools/src/app/api/health/route.ts b/apps/tools/src/app/api/health/route.ts new file mode 100644 index 00000000..ecb9ce04 --- /dev/null +++ b/apps/tools/src/app/api/health/route.ts @@ -0,0 +1,5 @@ +import { NextRequest } from "next/server"; + +export async function GET(request: NextRequest) { + return new Response("OK", { status: 200 }); +} diff --git a/apps/tools/src/app/base-page.tsx b/apps/tools/src/app/base-page.tsx new file mode 100644 index 00000000..61c827c7 --- /dev/null +++ b/apps/tools/src/app/base-page.tsx @@ -0,0 +1,53 @@ +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { cn } from "@/lib/utils"; + +interface BasePageProps { + children: React.ReactNode; + className?: string; + mainClassName?: string; + maxWidth?: "3xl" | "6xl" | "full"; + title?: string; + description?: string; +} + +export function BasePage({ + children, + className = "", + mainClassName = "", + maxWidth = "3xl", + title, + description, +}: BasePageProps) { + const maxWidthClass = { + "3xl": "max-w-3xl", + "6xl": "max-w-6xl", + full: "max-w-full", + }[maxWidth]; + + return ( +
+
+
+ {title && description && ( +
+

+ {title} +

+

+ {description} +

+
+ )} + {children} +
+
+
+ ); +} diff --git a/apps/tools/src/app/globals.css b/apps/tools/src/app/globals.css new file mode 100644 index 00000000..8d318925 --- /dev/null +++ b/apps/tools/src/app/globals.css @@ -0,0 +1,242 @@ +@import "tailwindcss"; + +/* Custom variant for dark mode */ +@custom-variant dark (&:where(.dark, .dark *)); + +/* Plugins */ +@plugin "@tailwindcss/typography"; +@plugin "tailwindcss-animate"; + +:root { + --background: hsl(0, 0%, 100%); + --foreground: hsl(0 0% 11%); + --card: hsl(0, 0%, 100%); + --card-foreground: hsl(0 0% 11%); + --popover: hsl(0, 0%, 100%); + --popover-foreground: hsl(0 0% 2%); + --primary: hsl(205, 84%, 47%); + --primary-foreground: hsl(0 0% 91%); + --secondary: hsl(216, 13%, 92%); + --secondary-foreground: hsl(0 0% 2%); + --muted: hsl(0 0% 85.1%); + --muted-foreground: hsl(0 0% 50%); + --accent: hsl(216, 13%, 92%); + --accent-foreground: hsl(0 0% 2%); + --destructive: hsl(0, 83%, 50%); + --destructive-foreground: hsl(0, 0%, 100%); + --border: hsl(0 0% 83%); + --input: hsl(0 0% 85.1%); + --ring: hsl(0, 0%, 55%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(0 0% 96.1%); + --sidebar-foreground: hsl(0 0% 2%); + --sidebar-primary: hsl(0 0% 2%); + --sidebar-primary-foreground: hsl(0 0% 91%); + --sidebar-accent: hsl(0 0% 85.1%); + --sidebar-accent-foreground: hsl(0 0% 2%); + --sidebar-border: hsl(0 0% 85.1%); + --sidebar-ring: hsl(0 0% 16.9%); + --panel-background: hsl(216 13% 92%); + --panel-accent: hsl(216, 8%, 86%); + + --radius: 1rem; +} +.dark { + --background: hsl(0 0% 4%); + --foreground: hsl(0 0% 89%); + --card: hsl(0 0% 4%); + --card-foreground: hsl(0 0% 89%); + --popover: hsl(0 0% 14.9%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(205, 84%, 53%); + --primary-foreground: hsl(0 0% 9%); + --secondary: hsl(0 0% 14.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(0 0% 14.9%); + --muted-foreground: hsl(0 0% 63.9%); + --accent: hsl(0 0% 14.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(0 100% 60%); + --destructive-foreground: hsl(0 0% 98%); + --border: hsl(0 0% 17%); + --input: hsl(0 0% 14.9%); + --ring: hsl(0 0% 83.1%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar-background: hsl(0 0% 3.9%); + --sidebar-foreground: hsl(0 0% 98%); + --sidebar-primary: hsl(0 0% 98%); + --sidebar-primary-foreground: hsl(0 0% 9%); + --sidebar-accent: hsl(0 0% 14.9%); + --sidebar-accent-foreground: hsl(0 0% 98%); + --sidebar-border: hsl(0 0% 14.9%); + --sidebar-ring: hsl(0 0% 83.1%); + --panel-background: hsl(0 0% 11%); + --panel-accent: hsl(0 0% 15%); +} + +@layer base { + /* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. + */ + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } + /* Other default base styles */ + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + /* Prevent back/forward swipe */ + overscroll-behavior-x: contain; + } +} + +@theme inline { + /* Responsive breakpoints */ + --breakpoint-xs: 30rem; + + /* Typography */ + --font-sans: var(--font-inter), sans-serif; + + /* Font sizes */ + --text-base: 0.95rem; + --text-base--line-height: calc(1.5 / 0.95); + --text-xs: 0.8rem; + --text-xs--line-height: calc(1 / 0.8); + + /* Border radius */ + --radius-lg: var(--radius); + --radius-md: calc(var(--radius) - 2px); + --radius-sm: calc(var(--radius) - 8px); + + /* Palette mapped to root design tokens */ + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + /* Chart colors */ + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + /* Sidebar */ + --color-sidebar: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* Panel */ + --color-panel: var(--panel-background); + --color-panel-accent: var(--panel-accent); + + /* Animations */ + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } +} + +@utility scrollbar-hidden { + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +} + +@utility scrollbar-x-hidden { + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:horizontal { + display: none; + } +} + +@utility scrollbar-y-hidden { + -ms-overflow-style: none; + scrollbar-width: none; + &::-webkit-scrollbar:vertical { + display: none; + } +} + +@utility scrollbar-thin { + &::-webkit-scrollbar { + width: 6px; + height: 8px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; + } + &::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); + } +} diff --git a/apps/tools/src/app/layout.tsx b/apps/tools/src/app/layout.tsx new file mode 100644 index 00000000..50cf5ffa --- /dev/null +++ b/apps/tools/src/app/layout.tsx @@ -0,0 +1,54 @@ +import { ThemeProvider } from "next-themes"; +import Script from "next/script"; +import "./globals.css"; +import { Toaster } from "../components/ui/sonner"; +import { TooltipProvider } from "../components/ui/tooltip"; +import { baseMetaData } from "./metadata"; +import { BotIdClient } from "botid/client"; +import { env } from "@opencut/env"; +import { Inter } from "next/font/google"; + +const siteFont = Inter({ subsets: ["latin"] }); + +export const metadata = baseMetaData; + +const protectedRoutes = [ + { + path: "/none", + method: "GET", + }, +]; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + + + + {children} + +