Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d5d95ea60 |
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Readability First
|
||||||
|
|
||||||
|
Optimize code for AI agents to understand and modify.
|
||||||
|
|
||||||
|
Never abbreviate. `event` not `e`, `element` not `el`. If it's easy to read, it's correct. In this case, "config" is better than "configuration" because it's shorter and is **still very readable**. "El" is not very readable.
|
||||||
@@ -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
|
||||||
@@ -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 formatter.
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
- Zero configuration required
|
||||||
|
- Subsecond performance
|
||||||
|
- Maximum type safety
|
||||||
|
- AI-friendly code generation
|
||||||
|
|
||||||
|
## Before Writing Code
|
||||||
|
|
||||||
|
1. Analyze existing patterns in the codebase
|
||||||
|
2. Consider edge cases and error scenarios
|
||||||
|
3. Follow the rules below strictly
|
||||||
|
4. Validate accessibility requirements
|
||||||
|
5. Avoid code duplication
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
### Accessibility (a11y)
|
||||||
|
|
||||||
|
- Always include a `title` element for icons unless there's text beside the icon.
|
||||||
|
- 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 `<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);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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!
|
||||||
@@ -1,11 +1,50 @@
|
|||||||
⚠️ 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
|
|
||||||
|
|
||||||
If this is a feature:
|
## Type of change
|
||||||
|
|
||||||
This PR will be closed. Please open an issue to discuss first.
|
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.
|
||||||
|
|||||||
@@ -27,35 +27,11 @@ jobs:
|
|||||||
NEXT_PUBLIC_SITE_URL: "http://localhost:3000"
|
NEXT_PUBLIC_SITE_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"
|
|
||||||
FREESOUND_CLIENT_ID: "placeholder"
|
|
||||||
FREESOUND_API_KEY: "placeholder"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust wasm target
|
|
||||||
run: rustup target add wasm32-unknown-unknown
|
|
||||||
|
|
||||||
- name: Install wasm-pack
|
|
||||||
uses: jetli/wasm-pack-action@v0.4.0
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
|
|
||||||
- name: Cache Rust build artifacts
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Build WASM
|
|
||||||
run: wasm-pack build rust/wasm --target bundler --out-dir pkg
|
|
||||||
|
|
||||||
- name: Install Bun
|
- name: Install Bun
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
|
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
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
|
# asdf version management
|
||||||
.moon/cache/
|
.tool-versions
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.cursorignore
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
.env*
|
||||||
|
!*.env.example
|
||||||
|
|
||||||
|
# cursor
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Twiggy
|
||||||
|
.cursor/rules/file-structure.mdc
|
||||||
|
|||||||
@@ -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: {}
|
|
||||||
@@ -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/*'
|
|
||||||
@@ -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"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
*.json
|
|
||||||
*.svg
|
|
||||||
*.xml
|
|
||||||
*.lock
|
|
||||||
**/dist/**
|
|
||||||
**/build/**
|
|
||||||
**/node_modules/**
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"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": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"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": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.biome": "explicit",
|
"source.fixAll.biome": "explicit",
|
||||||
"source.organizeImports.biome": "explicit"
|
"source.organizeImports.biome": "explicit"
|
||||||
},
|
},
|
||||||
"emmet.showExpandedAbbreviation": "never",
|
"emmet.showExpandedAbbreviation": "never",
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Privacy-first video editor, with a focus on simplicity and ease of use.
|
||||||
|
|
||||||
|
## Lib vs Utils
|
||||||
|
|
||||||
|
- `lib/` - domain logic (specific to this app)
|
||||||
|
- `utils/` - small helper utils (generic, could be copy-pasted into any other app)
|
||||||
|
|
||||||
|
## Core Editor System
|
||||||
|
|
||||||
|
The editor uses a **singleton EditorCore** that manages all editor state through specialized managers.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
EditorCore (singleton)
|
||||||
|
├── playback: PlaybackManager
|
||||||
|
├── timeline: TimelineManager
|
||||||
|
├── scene: SceneManager
|
||||||
|
├── project: ProjectManager
|
||||||
|
├── media: MediaManager
|
||||||
|
└── renderer: RendererManager
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use What
|
||||||
|
|
||||||
|
#### In React Components
|
||||||
|
|
||||||
|
**Always use the `useEditor()` hook:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useEditor } from '@/hooks/use-editor';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const editor = useEditor();
|
||||||
|
const tracks = editor.timeline.getTracks();
|
||||||
|
|
||||||
|
// Call methods
|
||||||
|
editor.timeline.addTrack({ type: 'media' });
|
||||||
|
|
||||||
|
// Display data (auto re-renders on changes)
|
||||||
|
return <div>{tracks.length} tracks</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The hook:
|
||||||
|
|
||||||
|
- Returns the singleton instance
|
||||||
|
- Subscribes to all manager changes
|
||||||
|
- Automatically re-renders when state changes
|
||||||
|
|
||||||
|
#### Outside React Components
|
||||||
|
|
||||||
|
**Use `EditorCore.getInstance()` directly:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In utilities, event handlers, or non-React code
|
||||||
|
import { EditorCore } from "@/core";
|
||||||
|
|
||||||
|
const editor = EditorCore.getInstance();
|
||||||
|
await editor.export({ format: "mp4", quality: "high" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actions System
|
||||||
|
|
||||||
|
Actions are the trigger layer for user-initiated operations. The single source of truth is `@/lib/actions/definitions.ts`.
|
||||||
|
|
||||||
|
**To add a new action:**
|
||||||
|
|
||||||
|
1. Add it to `ACTIONS` in `@/lib/actions/definitions.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const ACTIONS = {
|
||||||
|
"my-action": {
|
||||||
|
description: "What the action does",
|
||||||
|
category: "editing",
|
||||||
|
defaultShortcuts: ["ctrl+m"],
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add handler in `@/hooks/use-editor-actions.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useActionHandler(
|
||||||
|
"my-action",
|
||||||
|
() => {
|
||||||
|
// implementation
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**In components, use `invokeAction()` for user-triggered operations:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { invokeAction } from '@/lib/actions';
|
||||||
|
|
||||||
|
// Good - uses action system
|
||||||
|
const handleSplit = () => invokeAction("split-selected");
|
||||||
|
|
||||||
|
// Avoid - bypasses UX layer (toasts, validation feedback)
|
||||||
|
const handleSplit = () => editor.timeline.splitElements({ ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
Direct `editor.xxx()` calls are for internal use (commands, tests, complex multi-step operations).
|
||||||
|
|
||||||
|
## Commands System
|
||||||
|
|
||||||
|
Commands handle undo/redo. They live in `@/lib/commands/` organized by domain (timeline, media, scene).
|
||||||
|
|
||||||
|
Each command extends `Command` from `@/lib/commands/base-command` and implements:
|
||||||
|
|
||||||
|
- `execute()` - saves current state, then does the mutation
|
||||||
|
- `undo()` - restores the saved state
|
||||||
|
|
||||||
|
Actions and commands work together: actions are "what triggered this", commands are "how to do it (and undo it)".
|
||||||
@@ -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,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:
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,183 @@
|
|||||||
<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/logos/opencut/1k/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>
|
||||||
|
|
||||||
[](https://discord.gg/zmR9N35cjK)
|
## Why?
|
||||||
[](https://x.com/opencutapp)
|
|
||||||
[](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: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
|
## 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) and [fal.ai](https://fal.ai?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
|
||||||
|
|
||||||
|
<a href="https://vercel.com/oss">
|
||||||
|
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://fal.ai">
|
||||||
|
<img alt="Powered by fal.ai" src="https://img.shields.io/badge/Powered%20by-fal.ai-000000?style=flat&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCAxMEwxMy4wOSAxNS43NEwxMiAyMkwxMC45MSAxNS43NEw0IDEwTDEwLjkxIDguMjZMMTIgMloiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[](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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -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'
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
@@ -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`.
|
|
||||||
@@ -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'
|
|
||||||
@@ -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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 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:3000
|
||||||
|
NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
|
||||||
|
|
||||||
|
# Server
|
||||||
|
DATABASE_URL="postgresql://opencut:opencut@localhost:5432/opencut"
|
||||||
|
BETTER_AUTH_SECRET=your_better_auth_secret
|
||||||
|
|
||||||
|
UPSTASH_REDIS_REST_URL=http://localhost:8079
|
||||||
|
UPSTASH_REDIS_REST_TOKEN=example_token_here
|
||||||
|
|
||||||
|
MARBLE_WORKSPACE_KEY=your_workspace_key_here
|
||||||
|
|
||||||
|
FREESOUND_CLIENT_ID=your_client_id_here
|
||||||
|
FREESOUND_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
|
||||||
|
R2_ACCESS_KEY_ID=your_access_key_here
|
||||||
|
R2_SECRET_ACCESS_KEY=your_secret_key_here
|
||||||
|
R2_BUCKET_NAME=opencut-transcription # whatever you named your r2 bucket
|
||||||
|
|
||||||
|
MODAL_TRANSCRIPTION_URL=your_modal_url_here
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
node_modules
|
# Turborepo
|
||||||
.DS_Store
|
.turbo
|
||||||
dist
|
|
||||||
dist-ssr
|
# Env vars
|
||||||
*.local
|
.env*
|
||||||
.env
|
!.env.example
|
||||||
.nitro
|
|
||||||
.tanstack
|
.next/
|
||||||
.wrangler
|
|
||||||
.output
|
|
||||||
.vinxi
|
|
||||||
__unconfig*
|
|
||||||
todos.json
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"files.watcherExclude": {
|
|
||||||
"**/routeTree.gen.ts": true
|
|
||||||
},
|
|
||||||
"search.exclude": {
|
|
||||||
"**/routeTree.gen.ts": true
|
|
||||||
},
|
|
||||||
"files.readonlyInclude": {
|
|
||||||
"**/routeTree.gen.ts": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Config } from "drizzle-kit";
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
import { webEnv } from "@opencut/env/web";
|
||||||
|
|
||||||
|
// Load the right env file based on environment
|
||||||
|
if (webEnv.NODE_ENV === "production") {
|
||||||
|
dotenv.config({ path: ".env.production" });
|
||||||
|
} else {
|
||||||
|
dotenv.config({ path: ".env.local" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
schema: "./src/schema.ts",
|
||||||
|
dialect: "postgresql",
|
||||||
|
migrations: {
|
||||||
|
table: "drizzle_migrations",
|
||||||
|
},
|
||||||
|
dbCredentials: {
|
||||||
|
url: webEnv.DATABASE_URL,
|
||||||
|
},
|
||||||
|
out: "./migrations",
|
||||||
|
strict: webEnv.NODE_ENV === "production",
|
||||||
|
} satisfies Config;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
CREATE TABLE "accounts" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"account_id" text NOT NULL,
|
||||||
|
"provider_id" text NOT NULL,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"access_token" text,
|
||||||
|
"refresh_token" text,
|
||||||
|
"id_token" text,
|
||||||
|
"access_token_expires_at" timestamp,
|
||||||
|
"refresh_token_expires_at" timestamp,
|
||||||
|
"scope" text,
|
||||||
|
"password" text,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "accounts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
CREATE TABLE "sessions" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"expires_at" timestamp NOT NULL,
|
||||||
|
"token" text NOT NULL,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL,
|
||||||
|
"ip_address" text,
|
||||||
|
"user_agent" text,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
CONSTRAINT "sessions_token_unique" UNIQUE("token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"email_verified" boolean NOT NULL,
|
||||||
|
"image" text,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL,
|
||||||
|
CONSTRAINT "users_email_unique" UNIQUE("email")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
CREATE TABLE "verifications" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"identifier" text NOT NULL,
|
||||||
|
"value" text NOT NULL,
|
||||||
|
"expires_at" timestamp NOT NULL,
|
||||||
|
"created_at" timestamp,
|
||||||
|
"updated_at" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
CREATE TABLE "waitlist" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
CONSTRAINT "waitlist_email_unique" UNIQUE("email")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||||
|
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
{
|
||||||
|
"id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.accounts": {
|
||||||
|
"name": "accounts",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"accounts_user_id_users_id_fk": {
|
||||||
|
"name": "accounts_user_id_users_id_fk",
|
||||||
|
"tableFrom": "accounts",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": ["user_id"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"sessions_user_id_users_id_fk": {
|
||||||
|
"name": "sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": ["user_id"],
|
||||||
|
"columnsTo": ["id"],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"sessions_token_unique": {
|
||||||
|
"name": "sessions_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["token"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["email"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.verifications": {
|
||||||
|
"name": "verifications",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
},
|
||||||
|
"public.waitlist": {
|
||||||
|
"name": "waitlist",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"waitlist_email_unique": {
|
||||||
|
"name": "waitlist_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": ["email"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1750753385927,
|
||||||
|
"tag": "0000_brainy_saracen",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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'
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
@@ -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);
|
||||||
@@ -1,73 +1,98 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencut/web",
|
"name": "@opencut/web",
|
||||||
|
"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",
|
"@hugeicons/core-free-icons": "^3.1.1",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@hugeicons/react": "^1.1.4",
|
||||||
"@tanstack/react-devtools": "latest",
|
"@huggingface/transformers": "^3.8.1",
|
||||||
"@tanstack/react-router": "latest",
|
"@opencut/env": "workspace:*",
|
||||||
"@tanstack/react-router-devtools": "latest",
|
"@opencut/ui": "workspace:*",
|
||||||
"@tanstack/react-router-ssr-query": "latest",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@tanstack/react-start": "latest",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@tanstack/router-plugin": "^1.132.0",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@upstash/ratelimit": "^2.0.6",
|
||||||
|
"@upstash/redis": "^1.35.4",
|
||||||
|
"@vercel/analytics": "^1.4.1",
|
||||||
|
"aws4fetch": "^1.0.20",
|
||||||
|
"better-auth": "^1.2.7",
|
||||||
|
"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",
|
"drizzle-orm": "^0.44.2",
|
||||||
"input-otp": "^1.4.2",
|
"embla-carousel-react": "^8.5.1",
|
||||||
"lucide-react": "^1.14.0",
|
"eventemitter3": "^5.0.1",
|
||||||
"next-themes": "^0.4.6",
|
"feed": "^5.1.0",
|
||||||
"radix-ui": "^1.4.3",
|
"input-otp": "^1.4.1",
|
||||||
"react": "^19.2.0",
|
"lucide-react": "^0.562.0",
|
||||||
"react-day-picker": "^10.0.0",
|
"mediabunny": "^1.29.1",
|
||||||
"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": "16.1.3",
|
||||||
"recharts": "3.8.0",
|
"next-themes": "^0.4.4",
|
||||||
"shadcn": "^4.7.0",
|
"pg": "^8.16.2",
|
||||||
"sonner": "^2.0.7",
|
"postgres": "^3.4.5",
|
||||||
"tailwind-merge": "^3.5.0",
|
"radix-ui": "^1.4.2",
|
||||||
"tailwindcss": "^4.1.18",
|
"react": "^18.2.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"react-day-picker": "^8.10.1",
|
||||||
"vaul": "^1.1.2",
|
"react-dom": "^18.2.0",
|
||||||
"zod": "^4.4.3"
|
"react-hook-form": "^7.54.0",
|
||||||
|
"react-icons": "^5.4.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-phone-number-input": "^3.4.11",
|
||||||
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"recharts": "^2.14.1",
|
||||||
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
"rehype-parse": "^9.0.1",
|
||||||
|
"rehype-sanitize": "^6.0.0",
|
||||||
|
"rehype-slug": "^6.0.0",
|
||||||
|
"rehype-stringify": "^10.0.1",
|
||||||
|
"sonner": "^1.7.1",
|
||||||
|
"tailwind-merge": "^2.5.5",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"unified": "^11.0.5",
|
||||||
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
|
"vaul": "^1.1.1",
|
||||||
|
"wavesurfer.js": "^7.9.8",
|
||||||
|
"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",
|
"dotenv": "^16.5.0",
|
||||||
"typescript": "^6.0.2",
|
"postcss": "^8",
|
||||||
"vite": "^8.0.0",
|
"tailwindcss": "^4.1.11",
|
||||||
"vitest": "^4.1.5",
|
"tsx": "^4.7.1",
|
||||||
"wrangler": "^4.70.0"
|
"typescript": "^5.8.3"
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"onlyBuiltDependencies": [
|
|
||||||
"esbuild",
|
|
||||||
"lightningcss"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -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>
|
||||||
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 741 B |
|
After Width: | Height: | Size: 768 B |
|
After Width: | Height: | Size: 802 B |
|
After Width: | Height: | Size: 826 B |
|
After Width: | Height: | Size: 906 B |
|
After Width: | Height: | Size: 985 B |
|
After Width: | Height: | Size: 998 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 809 B |
|
After Width: | Height: | Size: 843 B |
|
After Width: | Height: | Size: 826 B |
|
After Width: | Height: | Size: 820 B |
|
After Width: | Height: | Size: 670 B |
|
After Width: | Height: | Size: 747 B |
|
After Width: | Height: | Size: 906 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 814 B |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -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 |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="512" height="513" viewBox="0 0 512 513" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M330.144 0.740479C339.014 0.740479 346.122 7.95256 346.971 16.7816C354.534 95.534 417.207 158.206 495.959 165.769C504.788 166.618 512 173.726 512 182.596V330.886C512 339.755 504.788 346.863 495.959 347.712C417.207 355.275 354.534 417.947 346.971 496.7C346.122 505.529 339.014 512.74 330.144 512.74H181.856C172.986 512.74 165.878 505.529 165.029 496.7C157.467 417.947 94.7934 355.275 16.041 347.712C7.21212 346.863 0 339.755 0 330.886V182.596C0 173.726 7.21216 166.618 16.0411 165.769C94.7935 158.206 157.467 95.534 165.029 16.7816C165.878 7.95256 172.986 0.740479 181.856 0.740479H330.144ZM102.8 256.307C102.8 341.495 171.781 410.554 256.874 410.554C341.966 410.554 410.948 341.495 410.948 256.307C410.948 171.119 341.966 102.06 256.874 102.06C171.781 102.06 102.8 171.119 102.8 256.307Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 960 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="588" height="512" viewBox="0 0 588 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M294 0L588 512H0L294 0Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 156 B |
@@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { auth } from "@/lib/auth/server";
|
||||||
|
import { toNextJsHandler } from "better-auth/next-js";
|
||||||
|
|
||||||
|
export const { POST, GET } = toNextJsHandler(auth);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export async function GET() {
|
||||||
|
return new Response("OK", { status: 200 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { webEnv } from "@opencut/env/web";
|
||||||
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { checkRateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
|
const searchParamsSchema = z.object({
|
||||||
|
q: z.string().max(500, "Query too long").optional(),
|
||||||
|
type: z.enum(["songs", "effects"]).optional(),
|
||||||
|
page: z.coerce.number().int().min(1).max(1000).default(1),
|
||||||
|
page_size: z.coerce.number().int().min(1).max(150).default(20),
|
||||||
|
sort: z
|
||||||
|
.enum(["downloads", "rating", "created", "score"])
|
||||||
|
.default("downloads"),
|
||||||
|
min_rating: z.coerce.number().min(0).max(5).default(3),
|
||||||
|
commercial_only: z.coerce.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
const freesoundResultSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
previews: z
|
||||||
|
.object({
|
||||||
|
"preview-hq-mp3": z.string().url(),
|
||||||
|
"preview-lq-mp3": z.string().url(),
|
||||||
|
"preview-hq-ogg": z.string().url(),
|
||||||
|
"preview-lq-ogg": z.string().url(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
download: z.string().url().optional(),
|
||||||
|
duration: z.number(),
|
||||||
|
filesize: z.number(),
|
||||||
|
type: z.string(),
|
||||||
|
channels: z.number(),
|
||||||
|
bitrate: z.number(),
|
||||||
|
bitdepth: z.number(),
|
||||||
|
samplerate: z.number(),
|
||||||
|
username: z.string(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
license: z.string(),
|
||||||
|
created: z.string(),
|
||||||
|
num_downloads: z.number().optional(),
|
||||||
|
avg_rating: z.number().optional(),
|
||||||
|
num_ratings: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const freesoundResponseSchema = z.object({
|
||||||
|
count: z.number(),
|
||||||
|
next: z.string().url().nullable(),
|
||||||
|
previous: z.string().url().nullable(),
|
||||||
|
results: z.array(freesoundResultSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const transformedResultSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
previewUrl: z.string().optional(),
|
||||||
|
downloadUrl: z.string().optional(),
|
||||||
|
duration: z.number(),
|
||||||
|
filesize: z.number(),
|
||||||
|
type: z.string(),
|
||||||
|
channels: z.number(),
|
||||||
|
bitrate: z.number(),
|
||||||
|
bitdepth: z.number(),
|
||||||
|
samplerate: z.number(),
|
||||||
|
username: z.string(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
license: z.string(),
|
||||||
|
created: z.string(),
|
||||||
|
downloads: z.number().optional(),
|
||||||
|
rating: z.number().optional(),
|
||||||
|
ratingCount: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiResponseSchema = z.object({
|
||||||
|
count: z.number(),
|
||||||
|
next: z.string().nullable(),
|
||||||
|
previous: z.string().nullable(),
|
||||||
|
results: z.array(transformedResultSchema),
|
||||||
|
query: z.string().optional(),
|
||||||
|
type: z.string(),
|
||||||
|
page: z.number(),
|
||||||
|
pageSize: z.number(),
|
||||||
|
sort: z.string(),
|
||||||
|
minRating: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildSortParameter({ query, sort }: { query?: string; sort: string }) {
|
||||||
|
if (!query) return `${sort}_desc`;
|
||||||
|
return sort === "score" ? "score" : `${sort}_desc`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEffectsFilters({
|
||||||
|
params,
|
||||||
|
min_rating,
|
||||||
|
commercial_only,
|
||||||
|
}: {
|
||||||
|
params: URLSearchParams;
|
||||||
|
min_rating: number;
|
||||||
|
commercial_only: boolean;
|
||||||
|
}) {
|
||||||
|
params.append("filter", "duration:[* TO 30.0]");
|
||||||
|
params.append("filter", `avg_rating:[${min_rating} TO *]`);
|
||||||
|
|
||||||
|
if (commercial_only) {
|
||||||
|
params.append(
|
||||||
|
"filter",
|
||||||
|
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.append(
|
||||||
|
"filter",
|
||||||
|
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformFreesoundResult(
|
||||||
|
result: z.infer<typeof freesoundResultSchema>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id: result.id,
|
||||||
|
name: result.name,
|
||||||
|
description: result.description,
|
||||||
|
url: result.url,
|
||||||
|
previewUrl:
|
||||||
|
result.previews?.["preview-hq-mp3"] ||
|
||||||
|
result.previews?.["preview-lq-mp3"],
|
||||||
|
downloadUrl: result.download,
|
||||||
|
duration: result.duration,
|
||||||
|
filesize: result.filesize,
|
||||||
|
type: result.type,
|
||||||
|
channels: result.channels,
|
||||||
|
bitrate: result.bitrate,
|
||||||
|
bitdepth: result.bitdepth,
|
||||||
|
samplerate: result.samplerate,
|
||||||
|
username: result.username,
|
||||||
|
tags: result.tags,
|
||||||
|
license: result.license,
|
||||||
|
created: result.created,
|
||||||
|
downloads: result.num_downloads || 0,
|
||||||
|
rating: result.avg_rating || 0,
|
||||||
|
ratingCount: result.num_ratings || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { limited } = await checkRateLimit({ request });
|
||||||
|
if (limited) {
|
||||||
|
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
const validationResult = searchParamsSchema.safeParse({
|
||||||
|
q: searchParams.get("q") || undefined,
|
||||||
|
type: searchParams.get("type") || undefined,
|
||||||
|
page: searchParams.get("page") || undefined,
|
||||||
|
page_size: searchParams.get("page_size") || undefined,
|
||||||
|
sort: searchParams.get("sort") || undefined,
|
||||||
|
min_rating: searchParams.get("min_rating") || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!validationResult.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Invalid parameters",
|
||||||
|
details: validationResult.error.flatten().fieldErrors,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
q: query,
|
||||||
|
type,
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
sort,
|
||||||
|
min_rating,
|
||||||
|
commercial_only,
|
||||||
|
} = validationResult.data;
|
||||||
|
|
||||||
|
if (type === "songs") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Songs are not available yet",
|
||||||
|
message:
|
||||||
|
"Song search functionality is coming soon. Try searching for sound effects instead.",
|
||||||
|
},
|
||||||
|
{ status: 501 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = "https://freesound.org/apiv2/search/text/";
|
||||||
|
|
||||||
|
const sortParam = buildSortParameter({ query, sort });
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
query: query || "",
|
||||||
|
token: webEnv.FREESOUND_API_KEY,
|
||||||
|
page: page.toString(),
|
||||||
|
page_size: pageSize.toString(),
|
||||||
|
sort: sortParam,
|
||||||
|
fields:
|
||||||
|
"id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEffectsSearch = type === "effects" || !type;
|
||||||
|
if (isEffectsSearch) {
|
||||||
|
applyEffectsFilters({ params, min_rating, commercial_only });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}?${params.toString()}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error("Freesound API error:", response.status, errorText);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to search sounds" },
|
||||||
|
{ status: response.status },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = await response.json();
|
||||||
|
|
||||||
|
const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
|
||||||
|
if (!freesoundValidation.success) {
|
||||||
|
console.error(
|
||||||
|
"Invalid Freesound API response:",
|
||||||
|
freesoundValidation.error,
|
||||||
|
);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid response from Freesound API" },
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = freesoundValidation.data;
|
||||||
|
|
||||||
|
const transformedResults = data.results.map(transformFreesoundResult);
|
||||||
|
|
||||||
|
const responseData = {
|
||||||
|
count: data.count,
|
||||||
|
next: data.next,
|
||||||
|
previous: data.previous,
|
||||||
|
results: transformedResults,
|
||||||
|
query: query || "",
|
||||||
|
type: type || "effects",
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sort,
|
||||||
|
minRating: min_rating,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseValidation = apiResponseSchema.safeParse(responseData);
|
||||||
|
if (!responseValidation.success) {
|
||||||
|
console.error(
|
||||||
|
"Invalid API response structure:",
|
||||||
|
responseValidation.error,
|
||||||
|
);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal response formatting error" },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(responseValidation.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching sounds:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { Footer } from "@/components/footer";
|
||||||
|
import { cn } from "@/utils/ui";
|
||||||
|
|
||||||
|
interface BasePageProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
mainClassName?: string;
|
||||||
|
maxWidth?: "3xl" | "6xl" | "full";
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BasePage({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
mainClassName = "",
|
||||||
|
maxWidth = "3xl",
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: BasePageProps) {
|
||||||
|
const maxWidthClass = {
|
||||||
|
"3xl": "max-w-3xl",
|
||||||
|
"6xl": "max-w-6xl",
|
||||||
|
full: "max-w-full",
|
||||||
|
}[maxWidth];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={cn("bg-background min-h-screen", className)}>
|
||||||
|
<Header />
|
||||||
|
<main
|
||||||
|
className={cn(
|
||||||
|
"relative container mx-auto flex flex-col gap-12 px-6 pt-12 pb-24 md:pt-24",
|
||||||
|
maxWidthClass,
|
||||||
|
mainClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title && description && (
|
||||||
|
<div className="flex flex-col gap-8 text-center">
|
||||||
|
<h1 className="text-5xl font-bold tracking-tight md:text-6xl">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mx-auto max-w-2xl text-xl leading-relaxed">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { BasePage } from "@/app/base-page";
|
||||||
|
import Prose from "@/components/ui/prose";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query";
|
||||||
|
import type { Author, Post } from "@/types/blog";
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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: Author) => author.name),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const data = await getPosts();
|
||||||
|
if (!data || !data.posts.length) return [];
|
||||||
|
|
||||||
|
return data.posts.map((post) => ({
|
||||||
|
slug: post.slug,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogPostPage({ params }: PageProps) {
|
||||||
|
const slug = (await params).slug;
|
||||||
|
const data = await getSinglePost({ slug });
|
||||||
|
if (!data || !data.post) return notFound();
|
||||||
|
|
||||||
|
const html = await processHtmlContent({ html: data.post.content });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasePage>
|
||||||
|
<PostHeader post={data.post} />
|
||||||
|
<Separator />
|
||||||
|
<PostContent html={html} />
|
||||||
|
</BasePage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostHeader({ post }: { post: Post }) {
|
||||||
|
const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{post.coverImage && <PostCoverImage post={post} />}
|
||||||
|
<PostMeta date={formattedDate} publishedAt={post.publishedAt} />
|
||||||
|
<PostTitle title={post.title} />
|
||||||
|
<PostAuthor author={post.authors[0]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostCoverImage({ post }: { post: Post }) {
|
||||||
|
return (
|
||||||
|
<div className="relative aspect-video overflow-hidden rounded-lg">
|
||||||
|
<Image
|
||||||
|
src={post.coverImage}
|
||||||
|
alt={post.title}
|
||||||
|
loading="eager"
|
||||||
|
fill
|
||||||
|
className="rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<time dateTime={publishedAt.toString()}>{date}</time>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostTitle({ title }: { title: string }) {
|
||||||
|
return (
|
||||||
|
<h1 className="text-5xl font-bold tracking-tight md:text-4xl">{title}</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostAuthor({ author }: { author?: Author }) {
|
||||||
|
if (!author) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Image
|
||||||
|
src={author.image}
|
||||||
|
alt={author.name}
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
loading="eager"
|
||||||
|
className="aspect-square size-8 shrink-0 rounded-full"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground">{author.name}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostContent({ html }: { html: string }) {
|
||||||
|
return (
|
||||||
|
<section className="pt-8">
|
||||||
|
<Prose html={html} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { BasePage } from "@/app/base-page";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { getPosts } from "@/lib/blog/query";
|
||||||
|
import type { Author, Post } from "@/types/blog";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Blog - OpenCut",
|
||||||
|
description:
|
||||||
|
"Read the latest news and updates about OpenCut, the free and open-source video editor.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Blog - OpenCut",
|
||||||
|
description:
|
||||||
|
"Read the latest news and updates about OpenCut, the free and open-source video editor.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function BlogPage() {
|
||||||
|
const data = await getPosts();
|
||||||
|
if (!data || !data.posts) return <div>No posts yet</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasePage
|
||||||
|
title="Blog"
|
||||||
|
description="Read the latest news and updates about OpenCut, the free and open-source video editor."
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{data.posts.map((post) => (
|
||||||
|
<div key={post.id} className="flex flex-col">
|
||||||
|
<BlogPostItem post={post} />
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</BasePage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlogPostItem({ post }: { post: Post }) {
|
||||||
|
return (
|
||||||
|
<Link href={`/blog/${post.slug}`}>
|
||||||
|
<div className="flex h-auto w-full items-center justify-between py-6 opacity-100 hover:opacity-75">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-xl font-semibold">{post.title}</h2>
|
||||||
|
<p className="text-muted-foreground">{post.description}</p>
|
||||||
|
</div>
|
||||||
|
{post.authors && post.authors.length > 0 && (
|
||||||
|
<AuthorList authors={post.authors} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthorList({ authors }: { authors: Author[] }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{authors.map((author) => (
|
||||||
|
<div key={author.id} className="flex items-center gap-2">
|
||||||
|
<Avatar className="size-6 shadow-sm">
|
||||||
|
<AvatarImage src={author.image} alt={author.name} />
|
||||||
|
<AvatarFallback className="text-xs">
|
||||||
|
{author.name.charAt(0).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { GitHubContributeSection } from "@/components/gitHub-contribute-section";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { EXTERNAL_TOOLS } from "@/constants/site-constants";
|
||||||
|
import { BasePage } from "../base-page";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Contributors - OpenCut",
|
||||||
|
description:
|
||||||
|
"Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
|
||||||
|
openGraph: {
|
||||||
|
title: "Contributors - OpenCut",
|
||||||
|
description:
|
||||||
|
"Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Contributor {
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
avatar_url: string;
|
||||||
|
html_url: string;
|
||||||
|
contributions: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getContributors(): Promise<Contributor[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": "OpenCut-Web-App",
|
||||||
|
},
|
||||||
|
next: { revalidate: 600 }, // 10 minutes
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("Failed to fetch contributors");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const contributors = (await response.json()) as Contributor[];
|
||||||
|
|
||||||
|
const filteredContributors = contributors.filter(
|
||||||
|
(contributor) => contributor.type === "User",
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredContributors;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching contributors:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ContributorsPage() {
|
||||||
|
const contributors = await getContributors();
|
||||||
|
const topContributors = contributors.slice(0, 2);
|
||||||
|
const otherContributors = contributors.slice(2);
|
||||||
|
const totalContributions = contributors.reduce(
|
||||||
|
(sum, c) => sum + c.contributions,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BasePage
|
||||||
|
title="Contributors"
|
||||||
|
description="Meet the amazing people who contribute to OpenCut, the free and open-source video editor."
|
||||||
|
>
|
||||||
|
<div className="-mt-4 flex items-center justify-center gap-8 text-sm">
|
||||||
|
<StatItem value={contributors.length} label="contributors" />
|
||||||
|
<StatItem value={totalContributions} label="contributions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto flex max-w-6xl flex-col gap-20">
|
||||||
|
{topContributors.length > 0 && (
|
||||||
|
<TopContributorsSection contributors={topContributors} />
|
||||||
|
)}
|
||||||
|
{otherContributors.length > 0 && (
|
||||||
|
<AllContributorsSection contributors={otherContributors} />
|
||||||
|
)}
|
||||||
|
<ExternalToolsSection />
|
||||||
|
<GitHubContributeSection
|
||||||
|
title="Join the community"
|
||||||
|
description="OpenCut is built by developers like you. Every contribution, no matter how small, helps make video editing more accessible for everyone."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BasePage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatItem({ value, label }: { value: number; label: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="bg-foreground size-2 rounded-full" />
|
||||||
|
<span className="font-medium">{value}</span>
|
||||||
|
<span className="text-muted-foreground">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopContributorsSection({
|
||||||
|
contributors,
|
||||||
|
}: {
|
||||||
|
contributors: Contributor[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<div className="flex flex-col gap-2 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold">Top contributors</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Leading the way in contributions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto flex w-full max-w-xl flex-col justify-center gap-6 md:flex-row">
|
||||||
|
{contributors.map((contributor) => (
|
||||||
|
<TopContributorCard key={contributor.id} contributor={contributor} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopContributorCard({ contributor }: { contributor: Contributor }) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={contributor.html_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex flex-col gap-6 p-8 text-center">
|
||||||
|
<Avatar className="mx-auto size-28">
|
||||||
|
<AvatarImage
|
||||||
|
src={contributor.avatar_url}
|
||||||
|
alt={`${contributor.login}'s avatar`}
|
||||||
|
/>
|
||||||
|
<AvatarFallback className="text-lg font-semibold">
|
||||||
|
{contributor.login.charAt(0).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-xl font-semibold">{contributor.login}</h3>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<span className="font-medium">{contributor.contributions}</span>
|
||||||
|
<span className="text-muted-foreground">contributions</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AllContributorsSection({
|
||||||
|
contributors,
|
||||||
|
}: {
|
||||||
|
contributors: Contributor[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-12">
|
||||||
|
<div className="flex flex-col gap-2 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold">All contributors</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Everyone who makes OpenCut better
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
|
||||||
|
{contributors.map((contributor) => (
|
||||||
|
<Link
|
||||||
|
key={contributor.id}
|
||||||
|
href={contributor.html_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="opacity-100 hover:opacity-70"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center gap-2 p-2">
|
||||||
|
<Avatar className="size-16">
|
||||||
|
<AvatarImage
|
||||||
|
src={contributor.avatar_url}
|
||||||
|
alt={`${contributor.login}'s avatar`}
|
||||||
|
/>
|
||||||
|
<AvatarFallback>
|
||||||
|
{contributor.login.charAt(0).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-sm font-medium">{contributor.login}</h3>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{contributor.contributions}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExternalToolsSection() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-10">
|
||||||
|
<div className="flex flex-col gap-2 text-center">
|
||||||
|
<h2 className="text-2xl font-semibold">External tools</h2>
|
||||||
|
<p className="text-muted-foreground">Tools we use to build OpenCut</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto grid max-w-4xl grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
{EXTERNAL_TOOLS.map((tool, index) => (
|
||||||
|
<Link
|
||||||
|
key={tool.url}
|
||||||
|
href={tool.url}
|
||||||
|
target="_blank"
|
||||||
|
className="block"
|
||||||
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
|
>
|
||||||
|
<Card className="h-full">
|
||||||
|
<CardContent className="flex items-center justify-center h-full flex-col gap-4 p-6 text-center">
|
||||||
|
<tool.icon className="size-8" />
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
<h3 className="text-lg font-semibold">{tool.name}</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import {
|
||||||
|
ResizablePanelGroup,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizableHandle,
|
||||||
|
} from "@/components/ui/resizable";
|
||||||
|
import { AssetsPanel } from "@/components/editor/panels/assets";
|
||||||
|
import { PropertiesPanel } from "@/components/editor/panels/properties";
|
||||||
|
import { Timeline } from "@/components/editor/timeline";
|
||||||
|
import { PreviewPanel } from "@/components/editor/panels/preview";
|
||||||
|
import { EditorHeader } from "@/components/editor/editor-header";
|
||||||
|
import { EditorProvider } from "@/components/providers/editor-provider";
|
||||||
|
import { Onboarding } from "@/components/editor/onboarding";
|
||||||
|
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
|
||||||
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
|
|
||||||
|
export default function Editor() {
|
||||||
|
const params = useParams();
|
||||||
|
const projectId = params.project_id as string;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorProvider projectId={projectId}>
|
||||||
|
<div className="bg-background flex h-screen w-screen flex-col overflow-hidden">
|
||||||
|
<EditorHeader />
|
||||||
|
<div className="min-h-0 min-w-0 flex-1">
|
||||||
|
<EditorLayout />
|
||||||
|
</div>
|
||||||
|
<Onboarding />
|
||||||
|
<MigrationDialog />
|
||||||
|
</div>
|
||||||
|
</EditorProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorLayout() {
|
||||||
|
const { panels, setPanel } = usePanelStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResizablePanelGroup
|
||||||
|
direction="vertical"
|
||||||
|
className="size-full gap-[0.18rem]"
|
||||||
|
onLayout={(sizes) => {
|
||||||
|
setPanel("mainContent", sizes[0] ?? panels.mainContent);
|
||||||
|
setPanel("timeline", sizes[1] ?? panels.timeline);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ResizablePanel
|
||||||
|
defaultSize={panels.mainContent}
|
||||||
|
minSize={30}
|
||||||
|
maxSize={85}
|
||||||
|
className="min-h-0"
|
||||||
|
>
|
||||||
|
<ResizablePanelGroup
|
||||||
|
direction="horizontal"
|
||||||
|
className="size-full gap-[0.19rem] px-3"
|
||||||
|
onLayout={(sizes) => {
|
||||||
|
setPanel("tools", sizes[0] ?? panels.tools);
|
||||||
|
setPanel("preview", sizes[1] ?? panels.preview);
|
||||||
|
setPanel("properties", sizes[2] ?? panels.properties);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ResizablePanel
|
||||||
|
defaultSize={panels.tools}
|
||||||
|
minSize={15}
|
||||||
|
maxSize={40}
|
||||||
|
className="min-w-0 rounded-sm"
|
||||||
|
>
|
||||||
|
<AssetsPanel />
|
||||||
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
|
||||||
|
<ResizablePanel
|
||||||
|
defaultSize={panels.preview}
|
||||||
|
minSize={30}
|
||||||
|
className="min-h-0 min-w-0 flex-1"
|
||||||
|
>
|
||||||
|
<PreviewPanel />
|
||||||
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
|
||||||
|
<ResizablePanel
|
||||||
|
defaultSize={panels.properties}
|
||||||
|
minSize={15}
|
||||||
|
maxSize={40}
|
||||||
|
className="min-w-0 rounded-sm"
|
||||||
|
>
|
||||||
|
<PropertiesPanel />
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
|
||||||
|
<ResizablePanel
|
||||||
|
defaultSize={panels.timeline}
|
||||||
|
minSize={15}
|
||||||
|
maxSize={70}
|
||||||
|
className="min-h-0 px-3 pb-3"
|
||||||
|
>
|
||||||
|
<Timeline />
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
@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(203, 100%, 50%);
|
||||||
|
--primary-hover: hsl(203, 100%, 45%);
|
||||||
|
--primary-foreground: hsl(0, 0%, 100%);
|
||||||
|
--secondary: hsl(216 13% 94%);
|
||||||
|
--secondary-foreground: hsl(0 0% 2%);
|
||||||
|
--muted: hsl(0 0% 85.1%);
|
||||||
|
--muted-foreground: hsl(0 0% 50%);
|
||||||
|
--accent: hsl(216, 13%, 88%);
|
||||||
|
--accent-foreground: hsl(0 0% 2%);
|
||||||
|
--destructive: hsl(0, 83%, 50%);
|
||||||
|
--destructive-foreground: hsl(0, 0%, 100%);
|
||||||
|
--constructive: hsl(141, 71%, 48%);
|
||||||
|
--constructive-foreground: hsl(0, 0%, 100%);
|
||||||
|
--border: hsl(0 0% 88%);
|
||||||
|
--input: hsl(0 0% 85.1%);
|
||||||
|
--ring: hsl(0, 0%, 55%);
|
||||||
|
--chart-1: hsl(220 70% 50%);
|
||||||
|
--chart-2: hsl(160 60% 45%);
|
||||||
|
--chart-3: hsl(30 80% 55%);
|
||||||
|
--chart-4: hsl(280 65% 60%);
|
||||||
|
--chart-5: hsl(340 75% 55%);
|
||||||
|
--sidebar-background: hsl(0 0% 96.1%);
|
||||||
|
--sidebar-foreground: hsl(0 0% 2%);
|
||||||
|
--sidebar-primary: hsl(0 0% 2%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 91%);
|
||||||
|
--sidebar-accent: hsl(0 0% 85.1%);
|
||||||
|
--sidebar-accent-foreground: hsl(0 0% 2%);
|
||||||
|
--sidebar-border: hsl(0 0% 85.1%);
|
||||||
|
--sidebar-ring: hsl(0 0% 16.9%);
|
||||||
|
--panel-background: hsl(216 13% 94%);
|
||||||
|
--panel-accent: hsl(216, 8%, 88%);
|
||||||
|
--sidebar: hsl(0 0% 98%);
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: hsl(0 0% 4%);
|
||||||
|
--foreground: hsl(0 0% 89%);
|
||||||
|
--card: hsl(0 0% 4%);
|
||||||
|
--card-foreground: hsl(0 0% 89%);
|
||||||
|
--popover: hsl(0 0% 14.9%);
|
||||||
|
--popover-foreground: hsl(0 0% 98%);
|
||||||
|
--primary: hsl(203, 100%, 50%);
|
||||||
|
--primary-hover: hsl(203, 100%, 45%);
|
||||||
|
--primary-foreground: hsl(0 0% 9%);
|
||||||
|
--secondary: hsl(0 0% 14.9%);
|
||||||
|
--secondary-foreground: hsl(0 0% 98%);
|
||||||
|
--muted: hsl(0 0% 14.9%);
|
||||||
|
--muted-foreground: hsl(0 0% 63.9%);
|
||||||
|
--accent: hsl(0, 0%, 28%);
|
||||||
|
--accent-foreground: hsl(0 0% 98%);
|
||||||
|
--destructive: hsl(0 83%, 55%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--constructive: hsl(141, 71%, 48%);
|
||||||
|
--constructive-foreground: hsl(0 0% 100%);
|
||||||
|
--border: hsl(0 0% 17%);
|
||||||
|
--input: hsl(0 0% 14.9%);
|
||||||
|
--ring: hsl(0 0% 83.1%);
|
||||||
|
--chart-1: hsl(220 70% 50%);
|
||||||
|
--chart-2: hsl(160 60% 45%);
|
||||||
|
--chart-3: hsl(30 80% 55%);
|
||||||
|
--chart-4: hsl(280 65% 60%);
|
||||||
|
--chart-5: hsl(340 75% 55%);
|
||||||
|
--sidebar-background: hsl(0 0% 3.9%);
|
||||||
|
--sidebar-foreground: hsl(0 0% 98%);
|
||||||
|
--sidebar-primary: hsl(0 0% 98%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 9%);
|
||||||
|
--sidebar-accent: hsl(0 0% 14.9%);
|
||||||
|
--sidebar-accent-foreground: hsl(0 0% 98%);
|
||||||
|
--sidebar-border: hsl(0 0% 14.9%);
|
||||||
|
--sidebar-ring: hsl(0 0% 83.1%);
|
||||||
|
--panel-background: hsl(0 0% 11%);
|
||||||
|
--panel-accent: hsl(0 0% 15%);
|
||||||
|
--sidebar: hsl(240 5.9% 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
/*
|
||||||
|
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||||
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
looks the same as it did with Tailwind CSS v3.
|
||||||
|
|
||||||
|
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-xl: 1.20rem;
|
||||||
|
--text-base: 0.92rem;
|
||||||
|
--text-base--line-height: calc(1.5 / 0.95);
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-xs--line-height: calc(1 / 0.8);
|
||||||
|
|
||||||
|
/* Border radius */
|
||||||
|
--radius-lg: 0.82rem;
|
||||||
|
--radius-md: 0.65rem;
|
||||||
|
--radius-sm: 0.35rem;
|
||||||
|
|
||||||
|
/* Palette mapped to root design tokens */
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary-hover: var(--primary-hover);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
|
||||||
|
--color-constructive: var(--constructive);
|
||||||
|
--color-constructive-foreground: var(--constructive-foreground);
|
||||||
|
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
|
/* Chart colors */
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--color-sidebar: var(--sidebar-background);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
/* Panel */
|
||||||
|
--color-panel: var(--panel-background);
|
||||||
|
--color-panel-accent: var(--panel-accent);
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
|
@keyframes accordion-down {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes accordion-up {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility scrollbar-hidden {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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 { webEnv } from "@opencut/env/web";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
|
||||||
|
const siteFont = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata = baseMetaData;
|
||||||
|
|
||||||
|
const protectedRoutes = [
|
||||||
|
{
|
||||||
|
path: "/none",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<BotIdClient protect={protectedRoutes} />
|
||||||
|
</head>
|
||||||
|
<body className={`${siteFont.className} font-sans antialiased`}>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
disableTransitionOnChange={true}
|
||||||
|
>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Toaster />
|
||||||
|
<Script
|
||||||
|
src="https://cdn.databuddy.cc/databuddy.js"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
async
|
||||||
|
data-client-id="UP-Wcoy5arxFeK7oyjMMZ"
|
||||||
|
data-disabled={webEnv.NODE_ENV === "development"}
|
||||||
|
data-track-attributes={false}
|
||||||
|
data-track-errors={true}
|
||||||
|
data-track-outgoing-links={false}
|
||||||
|
data-track-web-vitals={false}
|
||||||
|
data-track-sessions={false}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</TooltipProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||