Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab8af831b | ||
|
|
b59255cca8 | ||
|
|
a2b520fbb6 | ||
|
|
a53cd53f55 | ||
|
|
7ae51862b6 | ||
|
|
6c81db47c0 | ||
|
|
8768f6dd8f | ||
|
|
107d7ab714 | ||
|
|
b315020416 | ||
|
|
a5888e2087 | ||
|
|
b601ab4b24 | ||
|
|
6482461a63 | ||
|
|
f402fb8337 | ||
|
|
446e68e65e | ||
|
|
238750c025 | ||
|
|
6fdb1559aa | ||
|
|
3f2d5cfcaa | ||
|
|
b3d35fbc2a | ||
|
|
fbe3db74d7 | ||
|
|
d1f4cb615b | ||
|
|
19f1086492 | ||
|
|
8ab3aeba7c | ||
|
|
6423b352d6 | ||
|
|
7b942b363d | ||
|
|
8b8d65b509 | ||
|
|
23187b0b6a | ||
|
|
f66cdbfeb2 | ||
|
|
99137fe493 | ||
|
|
79dcec8a2e | ||
|
|
97f8d1968f | ||
|
|
ae1292a14b | ||
|
|
6ea5a156b6 | ||
|
|
401cc35743 | ||
|
|
b8d08df244 | ||
|
|
2ee0a44735 | ||
|
|
a9b9cf6bf5 | ||
|
|
d6622dc6b3 | ||
|
|
6ec818fc11 | ||
|
|
6daa69635b | ||
|
|
6b1f38a9a3 | ||
|
|
56ca09692a | ||
|
|
0b7597b31f | ||
|
|
e6c6193638 | ||
|
|
685d902740 | ||
|
|
eea6d43c82 | ||
|
|
6a22a3ecbd | ||
|
|
0dffefdd59 | ||
|
|
cceb835a84 | ||
|
|
f1b45b4328 | ||
|
|
7428dc75cf | ||
|
|
1301d027e9 | ||
|
|
f5f31b43df | ||
|
|
a709af0059 | ||
|
|
75eebf52bc | ||
|
|
1582423dbf | ||
|
|
148d38d7c9 | ||
|
|
3e89d29985 | ||
|
|
729d10592f | ||
|
|
9da365bc01 | ||
|
|
8bdc894690 | ||
|
|
0387bd4c80 | ||
|
|
b1d0b10354 | ||
|
|
94eed96cad | ||
|
|
794d4ae149 | ||
|
|
011f51051c | ||
|
|
c46d28891f | ||
|
|
66e4367bc4 | ||
|
|
8161de00e6 | ||
|
|
eb28942dab | ||
|
|
68f175d3be | ||
|
|
d5c1b7bf0e | ||
|
|
0868d9cf13 | ||
|
|
8a3577d2e7 | ||
|
|
ac43a5a73f | ||
|
|
6312fc5b07 | ||
|
|
2da93aa98f | ||
|
|
f4bd689f51 | ||
|
|
4f7dc5ee3a | ||
|
|
4e8b7d3745 | ||
|
|
ee372266ca | ||
|
|
e10b0a3355 | ||
|
|
3914ea9b71 | ||
|
|
370944f443 | ||
|
|
c839db9234 | ||
|
|
769a7e708a | ||
|
|
1fa5442f21 | ||
|
|
dc0a8e7c96 | ||
|
|
3db30c72e7 | ||
|
|
eca13e3e71 | ||
|
|
1943a4d18f | ||
|
|
3eccc2f9e3 | ||
|
|
368a20ddc2 | ||
|
|
64588ceeea | ||
|
|
8fe66090e7 | ||
|
|
91e1232112 | ||
|
|
f726d95295 | ||
|
|
78a0a27a72 | ||
|
|
b68bf00454 | ||
|
|
cd19a4409f | ||
|
|
f1ed84006a | ||
|
|
8d82ea9c0b | ||
|
|
6aa4ffe462 | ||
|
|
8a54e5d884 | ||
|
|
5e6c26f3c1 | ||
|
|
520c0b3264 | ||
|
|
607eaeacc7 | ||
|
|
6de6c7fd95 | ||
|
|
34c67ef8e8 | ||
|
|
5f2f00dde0 | ||
|
|
6db7ad0696 | ||
|
|
24a0d10a92 | ||
|
|
e3d4f3fa92 | ||
|
|
d85566ab8d | ||
|
|
3191e038d8 | ||
|
|
ba994d8bf9 | ||
|
|
270e2951b1 | ||
|
|
496f7d690d | ||
|
|
11ee0dbf27 | ||
|
|
2c344fa01d | ||
|
|
3d9d669cb9 | ||
|
|
7feb3b238c | ||
|
|
7d4e166678 | ||
|
|
cad88ee4d1 | ||
|
|
36efdf66e7 | ||
|
|
2272598d42 | ||
|
|
5a2aaf3601 | ||
|
|
108d3f4eca | ||
|
|
8230faf082 | ||
|
|
dbd68ff139 | ||
|
|
0d0d737563 | ||
|
|
f06a930fc1 | ||
|
|
532f050a64 | ||
|
|
d422b602c9 | ||
|
|
cbd1b82123 | ||
|
|
4f7d401a97 | ||
|
|
30709d7134 | ||
|
|
93a0afdf64 | ||
|
|
b8979c65d4 | ||
|
|
f47e85c3bb | ||
|
|
e43b859439 | ||
|
|
e4b67094e7 | ||
|
|
79df736431 | ||
|
|
c411edb7c1 | ||
|
|
2776adaae1 | ||
|
|
964ac82d5e | ||
|
|
05243616ea | ||
|
|
ccd552a1c0 | ||
|
|
fcc09e80e2 | ||
|
|
dedb13546a | ||
|
|
c56d6f8188 | ||
|
|
e62227914a | ||
|
|
44d8c4c876 | ||
|
|
20235d0724 | ||
|
|
0997466331 | ||
|
|
1bd9fac203 | ||
|
|
69da5bee0c | ||
|
|
cb99925d75 | ||
|
|
78c6c497f1 | ||
|
|
22725f71fa | ||
|
|
1a8a1e889a | ||
|
|
3516bd69f4 | ||
|
|
57fc0fd323 | ||
|
|
bd9ec023ee | ||
|
|
f4b9f40ab1 | ||
|
|
93cc5d50f3 | ||
|
|
8a1d37cf3e | ||
|
|
e579ae1202 | ||
|
|
b048b739bd | ||
|
|
dc82826951 | ||
|
|
608dc1a62f | ||
|
|
f1d5932c54 | ||
|
|
cc150c7cfb | ||
|
|
f8fb238b22 | ||
|
|
0488a6be45 | ||
|
|
0f5dca4f12 | ||
|
|
154b78c7d1 | ||
|
|
d23be079cc | ||
|
|
70a6d58b84 | ||
|
|
dcf54c1250 | ||
|
|
5cc6abc3a4 | ||
|
|
77a14b1023 | ||
|
|
1c3b22edec | ||
|
|
41562baeb5 | ||
|
|
df207e0f83 | ||
|
|
dc5956900e | ||
|
|
b095862cc0 | ||
|
|
0a6ede73ae | ||
|
|
7ce0018c9b | ||
|
|
9528d0053b | ||
|
|
689853312d | ||
|
|
5a1d9ef3f4 | ||
|
|
eb66e03f0e | ||
|
|
a2418751e6 | ||
|
|
5777a555f6 | ||
|
|
220ec757d4 | ||
|
|
585e28d49b | ||
|
|
1261e663e1 | ||
|
|
c34e01c63e | ||
|
|
3e75852ffe | ||
|
|
e95992b406 | ||
|
|
6df9d70b9b | ||
|
|
f8a0390827 | ||
|
|
d862e56e34 | ||
|
|
d08fbd73e1 | ||
|
|
4d127a038b | ||
|
|
82817cb50c | ||
|
|
a54b3ef076 | ||
|
|
6dd8191f4b | ||
|
|
722dcdd88f | ||
|
|
8db3bead13 | ||
|
|
39ea298a9c | ||
|
|
2c0fd4f86b | ||
|
|
5e2b829f8a | ||
|
|
b09635064a | ||
|
|
6018efc5ac | ||
|
|
7e2a913e13 | ||
|
|
63496d05a2 | ||
|
|
f2eed05d36 | ||
|
|
26d523ebad | ||
|
|
0be9275a2d | ||
|
|
a2d08719f9 | ||
|
|
ca3e5c98e6 | ||
|
|
5ca2de25c6 | ||
|
|
ed8815c755 | ||
|
|
24dac1232c | ||
|
|
46e55674de | ||
|
|
8e8f1c3596 | ||
|
|
4e0483e425 | ||
|
|
31dc78637f | ||
|
|
705faea56f | ||
|
|
21f12ff763 | ||
|
|
719e0d4398 | ||
|
|
df518cbf4e | ||
|
|
d9a214ef07 | ||
|
|
dc58c10e64 | ||
|
|
8fd9b96f80 | ||
|
|
5216f4398f | ||
|
|
3a2ec3c291 | ||
|
|
a87b344ba5 | ||
|
|
a635ae9e14 | ||
|
|
88bf59d6b4 | ||
|
|
d62eb6aa9e | ||
|
|
fbb00cf9cc | ||
|
|
a63cdcfe7e | ||
|
|
1b870fd9a0 | ||
|
|
c7a00b6af1 | ||
|
|
9b6a1f7973 | ||
|
|
eee3233c97 | ||
|
|
14ae9b4f11 | ||
|
|
565d2595da | ||
|
|
a526066b61 | ||
|
|
b0165da764 | ||
|
|
228e2e4342 | ||
|
|
81e7a7fb32 | ||
|
|
887d1795dd | ||
|
|
9261589ff3 | ||
|
|
db50b5214d | ||
|
|
e7dcb586c0 | ||
|
|
93bea01c9e | ||
|
|
a2727f2ae5 | ||
|
|
82f8f76a84 | ||
|
|
2074dde3e3 | ||
|
|
feaeec8dbe | ||
|
|
3b74cddbfb | ||
|
|
a0695dadaa | ||
|
|
7bd0b43746 | ||
|
|
77a3708213 | ||
|
|
9a00e8ac5b | ||
|
|
51234e673b | ||
|
|
13237a04fd | ||
|
|
35dd209b4e | ||
|
|
5e7c27f542 | ||
|
|
354f4dec67 | ||
|
|
c6bffaa243 | ||
|
|
f3af18e0c7 | ||
|
|
85f41756d1 | ||
|
|
fe964c4a4a | ||
|
|
216e3e0c39 | ||
|
|
a352eb1aab | ||
|
|
a9e93471a7 | ||
|
|
50885fb976 | ||
|
|
6226ceb052 | ||
|
|
cb8f003478 | ||
|
|
956f3d201b | ||
|
|
49426f19cd | ||
|
|
9b94f89def | ||
|
|
07c2289168 | ||
|
|
987153266e | ||
|
|
6da55c7aee | ||
|
|
f96192289b | ||
|
|
f75b5d3fd2 | ||
|
|
4d77e3f2bb | ||
|
|
ab64c48c92 | ||
|
|
d352e58570 | ||
|
|
baa5f33407 | ||
|
|
dbfecbba59 | ||
|
|
86a3592c88 | ||
|
|
744419469d | ||
|
|
1762dc077d | ||
|
|
a982304bb3 | ||
|
|
5616d4e9f3 | ||
|
|
71970d5f5f | ||
|
|
93d1e3383c | ||
|
|
fca99d6126 | ||
|
|
33a8705f0d | ||
|
|
fe10cee3eb | ||
|
|
1d05f5ac56 | ||
|
|
395e415cc4 | ||
|
|
e50a817b97 | ||
|
|
484ce47867 | ||
|
|
681f006b35 | ||
|
|
0282ef2223 | ||
|
|
dc0b57cd6a | ||
|
|
a218a52d2d | ||
|
|
1c2cf80188 | ||
|
|
cdd29eb1dc | ||
|
|
34cfeacdbb | ||
|
|
6d55a34586 | ||
|
|
c6666fd385 | ||
|
|
9b5fd6eb07 | ||
|
|
00cf7f6ab7 | ||
|
|
a82817a703 | ||
|
|
39b6ea5613 | ||
|
|
ae45ef8154 | ||
|
|
49c4fcb229 | ||
|
|
464a6e83b9 | ||
|
|
d0b4b1a5fc | ||
|
|
2257ca2e6f | ||
|
|
490fc170c0 | ||
|
|
feb8a07c16 | ||
|
|
7366cb27e9 | ||
|
|
34daad9df7 | ||
|
|
9bc35363b9 | ||
|
|
e84c0cfda6 | ||
|
|
1dbc22b2f4 | ||
|
|
ad794fa43b | ||
|
|
9ca32e5e39 | ||
|
|
5120e2efa6 | ||
|
|
1aec9c5cf5 | ||
|
|
e88a059222 | ||
|
|
c3f0d640ef | ||
|
|
7d5914353b | ||
|
|
ee7eaecc9f | ||
|
|
8ea3db833f | ||
|
|
22ea6b8925 | ||
|
|
dfccf905bb | ||
|
|
3e4584edde | ||
|
|
4cd15fcf00 | ||
|
|
748856f4c4 | ||
|
|
2759d7ed31 | ||
|
|
03c2b16064 | ||
|
|
e15ccefbe5 | ||
|
|
5e1e509c11 | ||
|
|
bfcf20e108 | ||
|
|
4f64ff50cd | ||
|
|
58f23e8f2a | ||
|
|
33f4a54768 | ||
|
|
1d55b903d7 | ||
|
|
af047ba4a7 | ||
|
|
ef3563745a | ||
|
|
d06b7c3b0f | ||
|
|
b7293dc552 | ||
|
|
82d92934cb | ||
|
|
b2a976ba82 | ||
|
|
02d4f9c314 | ||
|
|
1d31b7a586 | ||
|
|
e00ba706fe | ||
|
|
f820d9a894 | ||
|
|
7878c8bb7b | ||
|
|
1f618601c7 | ||
|
|
59c189ca27 | ||
|
|
7bf0984698 | ||
|
|
0173db9944 | ||
|
|
154353ee66 | ||
|
|
bee7aba960 | ||
|
|
a346bdf497 | ||
|
|
f483cb98cf | ||
|
|
60debe2ce5 | ||
|
|
37d4014808 | ||
|
|
e98e40028e | ||
|
|
454a308644 | ||
|
|
2d834a49d7 | ||
|
|
792fb6cdc0 | ||
|
|
17c2ba2fc8 |
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
description: Ultracite Rules - AI-Ready Formatter and Linter
|
|
||||||
globs: "**/*.{ts,tsx,js,jsx}"
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Project Context
|
|
||||||
|
|
||||||
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
|
|
||||||
|
|
||||||
## Key Principles
|
|
||||||
|
|
||||||
- Zero configuration required
|
|
||||||
- Subsecond performance
|
|
||||||
- Maximum type safety
|
|
||||||
- AI-friendly code generation
|
|
||||||
|
|
||||||
## Before Writing Code
|
|
||||||
|
|
||||||
1. Analyze existing patterns in the codebase
|
|
||||||
2. Consider edge cases and error scenarios
|
|
||||||
3. Follow the rules below strictly
|
|
||||||
4. Validate accessibility requirements
|
|
||||||
5. Avoid code duplication
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
### Accessibility (a11y)
|
|
||||||
|
|
||||||
- Always include a `title` element for icons unless there's text beside the icon.
|
|
||||||
- Always include a `type` attribute for button elements.
|
|
||||||
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
|
|
||||||
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
|
|
||||||
|
|
||||||
### Code Complexity and Quality
|
|
||||||
|
|
||||||
- Don't use primitive type aliases or misleading types.
|
|
||||||
- Don't use the comma operator.
|
|
||||||
- Use for...of statements instead of Array.forEach.
|
|
||||||
- Don't initialize variables to undefined.
|
|
||||||
- Use .flatMap() instead of map().flat() when possible.
|
|
||||||
|
|
||||||
### React and JSX Best Practices
|
|
||||||
|
|
||||||
- Don't import `React` itself.
|
|
||||||
- Don't define React components inside other components.
|
|
||||||
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
|
|
||||||
- Don't insert comments as text nodes.
|
|
||||||
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
|
|
||||||
|
|
||||||
### Function Parameters and Props
|
|
||||||
|
|
||||||
- Always use destructured props objects instead of individual parameters in functions.
|
|
||||||
- Example: `function helloWorld({ prop }: { prop: string })` instead of `function helloWorld(param: string)`.
|
|
||||||
- This applies to all functions, not just React components.
|
|
||||||
|
|
||||||
### Correctness and Safety
|
|
||||||
|
|
||||||
- Don't assign a value to itself.
|
|
||||||
- Avoid unused imports and variables.
|
|
||||||
- Don't use await inside loops.
|
|
||||||
- Don't hardcode sensitive data like API keys and tokens.
|
|
||||||
- Don't use the TypeScript directive @ts-ignore.
|
|
||||||
- Make sure the `preconnect` attribute is used when using Google Fonts.
|
|
||||||
- Don't use the `delete` operator.
|
|
||||||
- Don't use `require()` in TypeScript/ES modules - use proper `import` statements.
|
|
||||||
|
|
||||||
### TypeScript Best Practices
|
|
||||||
|
|
||||||
- Don't use TypeScript enums.
|
|
||||||
- Use either `T[]` or `Array<T>` consistently.
|
|
||||||
- Don't use the `any` type.
|
|
||||||
|
|
||||||
### Style and Consistency
|
|
||||||
|
|
||||||
- Don't use global `eval()`.
|
|
||||||
- Use `String.slice()` instead of `String.substr()` and `String.substring()`.
|
|
||||||
- Don't use `else` blocks when the `if` block breaks early.
|
|
||||||
- Put default function parameters and optional function parameters last.
|
|
||||||
- Use `new` when throwing an error.
|
|
||||||
- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`.
|
|
||||||
|
|
||||||
### Next.js Specific Rules
|
|
||||||
|
|
||||||
- Don't use `<img>` elements in Next.js projects.
|
|
||||||
- Don't use `<head>` elements in Next.js projects.
|
|
||||||
|
|
||||||
## Example: Error Handling
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Comprehensive error handling
|
|
||||||
try {
|
|
||||||
const result = await fetchData();
|
|
||||||
return { success: true, data: result };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("API call failed:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ Bad: Swallowing errors
|
|
||||||
try {
|
|
||||||
return await fetchData();
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
!apps/web/.env.example
|
|
||||||
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
|||||||
Examples of behavior that contributes to a positive environment for our
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the overall
|
- Focusing on what is best not just for us as individuals, but for the overall
|
||||||
community
|
community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
any kind
|
any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email address,
|
- Publishing others' private information, such as a physical or email address,
|
||||||
without their explicit permission
|
without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
# Contributing to OpenCut
|
|
||||||
|
|
||||||
Thank you for your interest in contributing to OpenCut! This document provides guidelines and instructions for contributing.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/) (v18 or later)
|
|
||||||
- [Bun](https://bun.sh/docs/installation)
|
|
||||||
(for `npm` alternative)
|
|
||||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
|
|
||||||
|
|
||||||
> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Clone your fork locally
|
|
||||||
3. Navigate to the web app directory: `cd apps/web`
|
|
||||||
4. Copy `.env.example` to `.env.local`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unix/Linux/Mac
|
|
||||||
cp .env.example .env.local
|
|
||||||
|
|
||||||
# Windows Command Prompt
|
|
||||||
copy .env.example .env.local
|
|
||||||
|
|
||||||
# Windows PowerShell
|
|
||||||
Copy-Item .env.example .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Install dependencies: `bun install`
|
|
||||||
6. Start the development server: `bun run dev`
|
|
||||||
|
|
||||||
> **Note:** If you see an error like `Unsupported URL Type "workspace:*"` when running `npm install`, you have two options:
|
|
||||||
>
|
|
||||||
> 1. Upgrade to a recent npm version (v9 or later), which has full workspace protocol support.
|
|
||||||
> 2. Use an alternative package manager such as **bun** or **pnpm**.
|
|
||||||
|
|
||||||
## What to Focus On
|
|
||||||
|
|
||||||
**🎯 Good Areas to Contribute:**
|
|
||||||
|
|
||||||
- Timeline functionality and UI improvements
|
|
||||||
- Project management features
|
|
||||||
- Performance optimizations
|
|
||||||
- Bug fixes in existing functionality
|
|
||||||
- UI/UX improvements
|
|
||||||
- Documentation and testing
|
|
||||||
|
|
||||||
**⚠️ Areas to Avoid:**
|
|
||||||
|
|
||||||
- Preview panel enhancements (text fonts, stickers, effects)
|
|
||||||
- Export functionality improvements
|
|
||||||
- Preview rendering optimizations
|
|
||||||
|
|
||||||
**Why?** We're currently planning a major refactor of the preview system. The current preview renders DOM elements (HTML), but we're moving to a binary rendering approach similar to CapCut. This new system will ensure consistency between preview and export, and provide much better performance and quality.
|
|
||||||
|
|
||||||
The current HTML-based preview is essentially a prototype - the binary approach will be the "real deal." To avoid wasted effort, please focus on other areas of the application until this refactor is complete.
|
|
||||||
|
|
||||||
If you're unsure whether your idea falls into the preview category, feel free to ask us [directly in discord](https://discord.gg/zmR9N35cjK) or create a GitHub issue!
|
|
||||||
|
|
||||||
## Development Setup
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
1. Start the database and Redis services:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From project root
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Navigate to the web app directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/web
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Copy `.env.example` to `.env.local`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unix/Linux/Mac
|
|
||||||
cp .env.example .env.local
|
|
||||||
|
|
||||||
# Windows Command Prompt
|
|
||||||
copy .env.example .env.local
|
|
||||||
|
|
||||||
# Windows PowerShell
|
|
||||||
Copy-Item .env.example .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Configure required environment variables in `.env.local`:
|
|
||||||
|
|
||||||
**Required Variables:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Database (matches docker-compose.yaml)
|
|
||||||
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
|
|
||||||
|
|
||||||
# Generate a secure secret for Better Auth
|
|
||||||
BETTER_AUTH_SECRET="your-generated-secret-here"
|
|
||||||
NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
|
|
||||||
|
|
||||||
# Redis (matches docker-compose.yaml)
|
|
||||||
UPSTASH_REDIS_REST_URL="http://localhost:8079"
|
|
||||||
UPSTASH_REDIS_REST_TOKEN="example_token"
|
|
||||||
|
|
||||||
# Development
|
|
||||||
NODE_ENV="development"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Generate BETTER_AUTH_SECRET:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unix/Linux/Mac
|
|
||||||
openssl rand -base64 32
|
|
||||||
|
|
||||||
# Windows PowerShell (simple method)
|
|
||||||
[System.Web.Security.Membership]::GeneratePassword(32, 0)
|
|
||||||
|
|
||||||
# Cross-platform (using Node.js)
|
|
||||||
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
||||||
|
|
||||||
# Or use an online generator: https://generate-secret.vercel.app/32
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Run database migrations: `bun run db:migrate`
|
|
||||||
6. Start the development server: `bun run dev`
|
|
||||||
|
|
||||||
## How to Contribute
|
|
||||||
|
|
||||||
### Reporting Bugs
|
|
||||||
|
|
||||||
- Use the bug report template
|
|
||||||
- Include steps to reproduce
|
|
||||||
- Provide screenshots if applicable
|
|
||||||
|
|
||||||
### Suggesting Features
|
|
||||||
|
|
||||||
- Use the feature request template
|
|
||||||
- Explain the use case
|
|
||||||
- Consider implementation details
|
|
||||||
|
|
||||||
### Code Contributions
|
|
||||||
|
|
||||||
1. Create a new branch: `git checkout -b feature/your-feature-name`
|
|
||||||
2. Make your changes
|
|
||||||
3. Navigate to the web app directory: `cd apps/web`
|
|
||||||
4. Run the linter: `bun run lint`
|
|
||||||
5. Format your code: `bunx biome format --write .`
|
|
||||||
6. Commit your changes with a descriptive message
|
|
||||||
7. Push to your fork and create a pull request
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
- We use Biome for code formatting and linting
|
|
||||||
- Run `bunx biome format --write .` from the `apps/web` directory to format code
|
|
||||||
- Run `bun run lint` from the `apps/web` directory to check for linting issues
|
|
||||||
- Follow the existing code patterns
|
|
||||||
|
|
||||||
## Pull Request Process
|
|
||||||
|
|
||||||
1. Fill out the pull request template completely
|
|
||||||
2. Link any related issues
|
|
||||||
3. Ensure CI passes
|
|
||||||
4. Request review from maintainers
|
|
||||||
5. Address any feedback
|
|
||||||
|
|
||||||
## Community
|
|
||||||
|
|
||||||
- Be respectful and inclusive
|
|
||||||
- Follow our Code of Conduct
|
|
||||||
- Help others in discussions and issues
|
|
||||||
|
|
||||||
Thank you for contributing!
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Create a report to help us improve
|
description: Create a report to help us improve
|
||||||
title: '[BUG] '
|
title: "[BUG] "
|
||||||
labels: bug
|
labels: bug
|
||||||
body:
|
body:
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest an idea for OpenCut
|
description: Suggest an idea for OpenCut
|
||||||
title: '[FEATURE] '
|
title: "[FEATURE] "
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -3,19 +3,23 @@
|
|||||||
Thanks for using OpenCut! If you need help, here are your options:
|
Thanks for using OpenCut! If you need help, here are your options:
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- Check our [README](../README.md) for basic setup instructions
|
- Check our [README](../README.md) for basic setup instructions
|
||||||
- Review the [Contributing Guidelines](CONTRIBUTING.md) for development setup
|
- Review the [Contributing Guidelines](CONTRIBUTING.md) for development setup
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
- **Bug reports**: Use the bug report template
|
- **Bug reports**: Use the bug report template
|
||||||
- **Feature requests**: Use the feature request template
|
- **Feature requests**: Use the feature request template
|
||||||
- **Questions**: Use GitHub Discussions for general questions
|
- **Questions**: Use GitHub Discussions for general questions
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
- Join our discussions on GitHub
|
- Join our discussions on GitHub
|
||||||
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md)
|
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
|
||||||
## Response Times
|
## Response Times
|
||||||
|
|
||||||
- Issues are typically triaged within 2-3 business days
|
- Issues are typically triaged within 2-3 business days
|
||||||
- Feature requests may take longer to evaluate
|
- Feature requests may take longer to evaluate
|
||||||
- Security issues are handled with priority
|
- Security issues are handled with priority
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ applyTo: "**/*.{ts,tsx,js,jsx}"
|
|||||||
---
|
---
|
||||||
|
|
||||||
# Project Context
|
# Project Context
|
||||||
|
|
||||||
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
|
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
|
||||||
|
|
||||||
## Key Principles
|
## Key Principles
|
||||||
|
|
||||||
- Zero configuration required
|
- Zero configuration required
|
||||||
- Subsecond performance
|
- Subsecond performance
|
||||||
- Maximum type safety
|
- Maximum type safety
|
||||||
- AI-friendly code generation
|
- AI-friendly code generation
|
||||||
|
|
||||||
## Before Writing Code
|
## Before Writing Code
|
||||||
|
|
||||||
1. Analyze existing patterns in the codebase
|
1. Analyze existing patterns in the codebase
|
||||||
2. Consider edge cases and error scenarios
|
2. Consider edge cases and error scenarios
|
||||||
3. Follow the rules below strictly
|
3. Follow the rules below strictly
|
||||||
@@ -20,6 +23,7 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
### Accessibility (a11y)
|
### Accessibility (a11y)
|
||||||
|
|
||||||
- Don't use `accessKey` attribute on any HTML element.
|
- Don't use `accessKey` attribute on any HTML element.
|
||||||
- Don't set `aria-hidden="true"` on focusable elements.
|
- Don't set `aria-hidden="true"` on focusable elements.
|
||||||
- Don't add ARIA roles, states, and properties to elements that don't support them.
|
- Don't add ARIA roles, states, and properties to elements that don't support them.
|
||||||
@@ -56,6 +60,7 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
- Use correct ISO language/country codes for the `lang` attribute.
|
- Use correct ISO language/country codes for the `lang` attribute.
|
||||||
|
|
||||||
### Code Complexity and Quality
|
### Code Complexity and Quality
|
||||||
|
|
||||||
- Don't use consecutive spaces in regular expression literals.
|
- Don't use consecutive spaces in regular expression literals.
|
||||||
- Don't use the `arguments` object.
|
- Don't use the `arguments` object.
|
||||||
- Don't use primitive type aliases or misleading types.
|
- Don't use primitive type aliases or misleading types.
|
||||||
@@ -111,6 +116,7 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
- Don't use literal numbers that lose precision.
|
- Don't use literal numbers that lose precision.
|
||||||
|
|
||||||
### React and JSX Best Practices
|
### React and JSX Best Practices
|
||||||
|
|
||||||
- Don't use the return value of React.render.
|
- Don't use the return value of React.render.
|
||||||
- Make sure all dependencies are correctly specified in React hooks.
|
- Make sure all dependencies are correctly specified in React hooks.
|
||||||
- Make sure all React hooks are called from the top level of component functions.
|
- Make sure all React hooks are called from the top level of component functions.
|
||||||
@@ -129,6 +135,7 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
- Watch out for possible "wrong" semicolons inside JSX elements.
|
- Watch out for possible "wrong" semicolons inside JSX elements.
|
||||||
|
|
||||||
### Correctness and Safety
|
### Correctness and Safety
|
||||||
|
|
||||||
- Don't assign a value to itself.
|
- Don't assign a value to itself.
|
||||||
- Don't return a value from a setter.
|
- Don't return a value from a setter.
|
||||||
- Don't compare expressions that modify string case with non-compliant values.
|
- Don't compare expressions that modify string case with non-compliant values.
|
||||||
@@ -153,7 +160,7 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
- Don't use bitwise operators.
|
- Don't use bitwise operators.
|
||||||
- Don't use expressions where the operation doesn't change the value.
|
- Don't use expressions where the operation doesn't change the value.
|
||||||
- Make sure Promise-like statements are handled appropriately.
|
- Make sure Promise-like statements are handled appropriately.
|
||||||
- Don't use __dirname and __filename in the global scope.
|
- Don't use **dirname and **filename in the global scope.
|
||||||
- Prevent import cycles.
|
- Prevent import cycles.
|
||||||
- Don't use configured elements.
|
- Don't use configured elements.
|
||||||
- Don't hardcode sensitive data like API keys and tokens.
|
- Don't hardcode sensitive data like API keys and tokens.
|
||||||
@@ -184,6 +191,7 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
- Don't use `target="_blank"` without `rel="noopener"`.
|
- Don't use `target="_blank"` without `rel="noopener"`.
|
||||||
|
|
||||||
### TypeScript Best Practices
|
### TypeScript Best Practices
|
||||||
|
|
||||||
- Don't use TypeScript enums.
|
- Don't use TypeScript enums.
|
||||||
- Don't export imported variables.
|
- Don't export imported variables.
|
||||||
- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
|
- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
|
||||||
@@ -208,6 +216,7 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
|
- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
|
||||||
|
|
||||||
### Style and Consistency
|
### Style and Consistency
|
||||||
|
|
||||||
- Don't use global `eval()`.
|
- Don't use global `eval()`.
|
||||||
- Don't use callbacks in asynchronous tests and hooks.
|
- Don't use callbacks in asynchronous tests and hooks.
|
||||||
- Don't use negation in `if` statements that have `else` clauses.
|
- Don't use negation in `if` statements that have `else` clauses.
|
||||||
@@ -295,30 +304,34 @@ Ultracite enforces strict type safety, accessibility standards, and consistent c
|
|||||||
- Make sure to use the "use strict" directive in script files.
|
- Make sure to use the "use strict" directive in script files.
|
||||||
|
|
||||||
### Next.js Specific Rules
|
### Next.js Specific Rules
|
||||||
|
|
||||||
- Don't use `<img>` elements in Next.js projects.
|
- Don't use `<img>` elements in Next.js projects.
|
||||||
- Don't use `<head>` elements in Next.js projects.
|
- Don't use `<head>` elements in Next.js projects.
|
||||||
- Don't import next/document outside of pages/_document.jsx in Next.js projects.
|
- Don't import next/document outside of pages/\_document.jsx in Next.js projects.
|
||||||
- Don't use the next/head module in pages/_document.js on Next.js projects.
|
- Don't use the next/head module in pages/\_document.js on Next.js projects.
|
||||||
|
|
||||||
### Testing Best Practices
|
### Testing Best Practices
|
||||||
|
|
||||||
- Don't use export or module.exports in test files.
|
- Don't use export or module.exports in test files.
|
||||||
- Don't use focused tests.
|
- Don't use focused tests.
|
||||||
- Make sure the assertion function, like expect, is placed inside an it() function call.
|
- Make sure the assertion function, like expect, is placed inside an it() function call.
|
||||||
- Don't use disabled tests.
|
- Don't use disabled tests.
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
- `npx ultracite init` - Initialize Ultracite in your project
|
- `npx ultracite init` - Initialize Ultracite in your project
|
||||||
- `npx ultracite format` - Format and fix code automatically
|
- `npx ultracite format` - Format and fix code automatically
|
||||||
- `npx ultracite lint` - Check for issues without fixing
|
- `npx ultracite lint` - Check for issues without fixing
|
||||||
|
|
||||||
## Example: Error Handling
|
## Example: Error Handling
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ✅ Good: Comprehensive error handling
|
// ✅ Good: Comprehensive error handling
|
||||||
try {
|
try {
|
||||||
const result = await fetchData();
|
const result = await fetchData();
|
||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API call failed:', error);
|
console.error("API call failed:", error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,11 @@
|
|||||||
## Description
|
⚠️ READ BEFORE SUBMITTING ⚠️
|
||||||
|
|
||||||
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
|
We are not currently accepting PRs except for critical bugs.
|
||||||
|
|
||||||
Fixes # (issue)
|
If this is a bug fix:
|
||||||
|
- [ ] I've opened an issue first
|
||||||
|
- [ ] This was approved by a maintainer
|
||||||
|
|
||||||
## Type of change
|
If this is a feature:
|
||||||
|
|
||||||
Please delete options that are not relevant.
|
This PR will be closed. Please open an issue to discuss first.
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
|
||||||
- [ ] This change requires a documentation update
|
|
||||||
- [ ] Performance improvement
|
|
||||||
- [ ] Code refactoring
|
|
||||||
- [ ] Tests
|
|
||||||
|
|
||||||
## How Has This Been Tested?
|
|
||||||
|
|
||||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
|
||||||
|
|
||||||
- [ ] Test A
|
|
||||||
- [ ] Test B
|
|
||||||
|
|
||||||
**Test Configuration**:
|
|
||||||
* Node version:
|
|
||||||
* Browser (if applicable):
|
|
||||||
* Operating System:
|
|
||||||
|
|
||||||
## Screenshots (if applicable)
|
|
||||||
|
|
||||||
Add screenshots to help explain your changes.
|
|
||||||
|
|
||||||
## Checklist:
|
|
||||||
|
|
||||||
- [ ] My code follows the style guidelines of this project
|
|
||||||
- [ ] I have performed a self-review of my code
|
|
||||||
- [ ] I have added screenshots if ui has been changed
|
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
|
||||||
- [ ] I have made corresponding changes to the documentation
|
|
||||||
- [ ] My changes generate no new warnings
|
|
||||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
|
||||||
- [ ] New and existing unit tests pass locally with my changes
|
|
||||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
|
||||||
|
|
||||||
## Additional context
|
|
||||||
|
|
||||||
Add any other context about the pull request here.
|
|
||||||
@@ -15,43 +15,23 @@ on:
|
|||||||
- "*.md"
|
- "*.md"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
ci:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
env:
|
|
||||||
DATABASE_URL: "postgresql://opencut:opencutthegoat@localhost:5432/opencut"
|
|
||||||
BETTER_AUTH_SECRET: "supersecret"
|
|
||||||
NEXT_PUBLIC_BETTER_AUTH_URL: "http://localhost:3000"
|
|
||||||
UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url"
|
|
||||||
UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
|
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.18
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Cache Bun modules
|
- name: Setup toolchain
|
||||||
uses: actions/cache@v4
|
uses: moonrepo/setup-toolchain@v0
|
||||||
with:
|
with:
|
||||||
path: ~/.bun/install/cache
|
auto-install: true
|
||||||
key: ${{ runner.os }}-bun-1.2.18-${{ hashFiles('apps/web/bun.lock') }}
|
auto-setup: true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Run CI
|
||||||
working-directory: apps/web
|
run: moon ci
|
||||||
run: bun install
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
working-directory: apps/web
|
|
||||||
run: bun run build
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
working-directory: apps/web
|
|
||||||
run: echo "No tests implemented yet"
|
|
||||||
continue-on-error: true
|
|
||||||
|
|||||||
@@ -1,40 +1,10 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
node_modules/
|
||||||
|
dist/
|
||||||
|
target/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
docs/
|
||||||
|
|
||||||
# dependencies
|
# moon runtime cache
|
||||||
/apps/web/node_modules
|
.moon/cache/
|
||||||
|
|
||||||
# next.js
|
|
||||||
/apps/web/.next/
|
|
||||||
/apps/web/out/
|
|
||||||
|
|
||||||
# debug
|
|
||||||
/apps/web/npm-debug.log*
|
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
|
||||||
/apps/web/.env*
|
|
||||||
!/apps/web/.env.example
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
/apps/web/next-env.d.ts
|
|
||||||
|
|
||||||
# asdf version management
|
|
||||||
.tool-versions
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
.cursorignore
|
|
||||||
.turbo
|
|
||||||
|
|
||||||
*.env
|
|
||||||
|
|
||||||
# cursor
|
|
||||||
bun.lockb
|
|
||||||
apps/transcription/__pycache__
|
|
||||||
apps/transcription/env
|
|
||||||
|
|
||||||
apps/bg-remover/env
|
|
||||||
|
|
||||||
# Rust
|
|
||||||
/target
|
|
||||||
Cargo.lock
|
|
||||||
apps/core/target/
|
|
||||||
apps/core/pkg/
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
npx ultracite format
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
$schema: 'https://moonrepo.dev/schemas/toolchains.json'
|
||||||
|
|
||||||
|
# javascript must be enabled alongside bun
|
||||||
|
javascript: {}
|
||||||
|
|
||||||
|
bun:
|
||||||
|
version: '1.3.11'
|
||||||
|
# Auto-install deps when lockfile/manifest changes
|
||||||
|
installDependencies: true
|
||||||
|
|
||||||
|
# Version is inherited from .prototools (versionFromPrototools)
|
||||||
|
rust: {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
$schema: 'https://moonrepo.dev/schemas/workspace.json'
|
||||||
|
|
||||||
|
# Discover all projects under apps/ automatically.
|
||||||
|
# When shared crates land under crates/, add a 'crates/*' glob here.
|
||||||
|
projects:
|
||||||
|
- 'apps/*'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# proto pins tool versions workspace-wide.
|
||||||
|
# Every developer and CI machine gets the exact same versions automatically.
|
||||||
|
moon = "2.3.3"
|
||||||
|
bun = "1.3.11"
|
||||||
|
rust = "1.97.0"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
*.json
|
||||||
|
*.svg
|
||||||
|
*.xml
|
||||||
|
*.lock
|
||||||
|
**/dist/**
|
||||||
|
**/build/**
|
||||||
|
**/node_modules/**
|
||||||
@@ -1,14 +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": "biomejs.biome"
|
"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,
|
||||||
"emmet.showExpandedAbbreviation": "never",
|
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.biome": "explicit",
|
"source.fixAll.biome": "explicit",
|
||||||
"source.organizeImports.biome": "explicit"
|
"source.organizeImports.biome": "explicit"
|
||||||
|
},
|
||||||
|
"emmet.showExpandedAbbreviation": "never",
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
OpenCut is a free, open-source video editor built with Next.js, focusing on privacy (no server processing), multi-track timeline editing, and real-time preview. The project is a monorepo using Turborepo with multiple apps including a web application, desktop app (Tauri), background remover tools, and transcription services.
|
|
||||||
|
|
||||||
## Essential Commands
|
|
||||||
|
|
||||||
**Development:**
|
|
||||||
```bash
|
|
||||||
# Root level development
|
|
||||||
bun dev # Start all apps in development mode
|
|
||||||
bun build # Build all apps
|
|
||||||
bun lint # Lint all code using Ultracite
|
|
||||||
bun format # Format all code using Ultracite
|
|
||||||
|
|
||||||
# Web app specific (from apps/web/)
|
|
||||||
cd apps/web
|
|
||||||
bun run dev # Start Next.js development server with Turbopack
|
|
||||||
bun run build # Build for production
|
|
||||||
bun run lint # Run Biome linting
|
|
||||||
bun run lint:fix # Fix linting issues automatically
|
|
||||||
bun run format # Format code with Biome
|
|
||||||
|
|
||||||
# Database operations (from apps/web/)
|
|
||||||
bun run db:generate # Generate Drizzle migrations
|
|
||||||
bun run db:migrate # Run migrations
|
|
||||||
bun run db:push:local # Push schema to local development database
|
|
||||||
bun run db:push:prod # Push schema to production database
|
|
||||||
```
|
|
||||||
|
|
||||||
**Testing:**
|
|
||||||
- No unified test commands are currently configured
|
|
||||||
- Individual apps may have their own test setups
|
|
||||||
|
|
||||||
## Architecture & Key Components
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
The application uses **Zustand** for state management with separate stores for different concerns:
|
|
||||||
- **editor-store.ts**: Canvas presets, layout guides, app initialization
|
|
||||||
- **timeline-store.ts**: Timeline tracks, elements, playback state
|
|
||||||
- **media-store.ts**: Media files and asset management
|
|
||||||
- **playback-store.ts**: Video playback controls and timing
|
|
||||||
- **project-store.ts**: Project-level data and persistence
|
|
||||||
- **panel-store.ts**: UI panel visibility and layout
|
|
||||||
- **keybindings-store.ts**: Keyboard shortcut management
|
|
||||||
- **sounds-store.ts**: Audio effects and sound management
|
|
||||||
- **stickers-store.ts**: Sticker/graphics management
|
|
||||||
|
|
||||||
### Storage System
|
|
||||||
**Multi-layer storage approach:**
|
|
||||||
- **IndexedDB**: Projects, saved sounds, and structured data
|
|
||||||
- **OPFS (Origin Private File System)**: Large media files for better performance
|
|
||||||
- **Storage Service** (`lib/storage/`): Abstraction layer managing both storage types
|
|
||||||
|
|
||||||
### Editor Architecture
|
|
||||||
**Core editor components:**
|
|
||||||
- **Timeline Canvas**: Custom canvas-based timeline with tracks and elements
|
|
||||||
- **Preview Panel**: Real-time video preview (currently DOM-based, planned binary refactor)
|
|
||||||
- **Media Panel**: Asset management with drag-and-drop support
|
|
||||||
- **Properties Panel**: Context-sensitive element properties
|
|
||||||
|
|
||||||
### Media Processing
|
|
||||||
- **FFmpeg Integration**: Client-side video processing using @ffmpeg/ffmpeg
|
|
||||||
- **Background Removal**: Python-based tools with multiple AI models (U2Net, SAM, Gemini)
|
|
||||||
- **Transcription**: Separate service for audio-to-text conversion
|
|
||||||
|
|
||||||
## Development Focus Areas
|
|
||||||
|
|
||||||
**✅ Recommended contribution areas:**
|
|
||||||
- Timeline functionality and UI improvements
|
|
||||||
- Project management features
|
|
||||||
- Performance optimizations
|
|
||||||
- Bug fixes in existing functionality
|
|
||||||
- UI/UX improvements outside preview panel
|
|
||||||
- Documentation and testing
|
|
||||||
|
|
||||||
**⚠️ Areas to avoid (pending refactor):**
|
|
||||||
- Preview panel enhancements (fonts, stickers, effects)
|
|
||||||
- Export functionality improvements
|
|
||||||
- Preview rendering optimizations
|
|
||||||
|
|
||||||
**Reason:** The preview system is planned for a major refactor from DOM-based rendering to binary rendering for consistency with export and better performance.
|
|
||||||
|
|
||||||
## Code Quality Standards
|
|
||||||
|
|
||||||
**Linting & Formatting:**
|
|
||||||
- Uses **Biome** for JavaScript/TypeScript linting and formatting
|
|
||||||
- Extends **Ultracite** configuration for strict type safety and AI-friendly code
|
|
||||||
- Comprehensive accessibility (a11y) rules enforced
|
|
||||||
- Zero configuration approach with subsecond performance
|
|
||||||
|
|
||||||
**Key coding standards from Ultracite:**
|
|
||||||
- Strict TypeScript with no `any` types
|
|
||||||
- No React imports (uses automatic JSX runtime)
|
|
||||||
- Comprehensive accessibility requirements
|
|
||||||
- Use `for...of` instead of `Array.forEach`
|
|
||||||
- No TypeScript enums, use const objects
|
|
||||||
- Always include error handling with try-catch
|
|
||||||
|
|
||||||
## Environment Setup
|
|
||||||
|
|
||||||
**Required environment variables (apps/web/.env.local):**
|
|
||||||
```bash
|
|
||||||
# Database
|
|
||||||
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
BETTER_AUTH_SECRET="your-generated-secret-here"
|
|
||||||
BETTER_AUTH_URL="http://localhost:3000"
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
UPSTASH_REDIS_REST_URL="http://localhost:8079"
|
|
||||||
UPSTASH_REDIS_REST_TOKEN="example_token"
|
|
||||||
|
|
||||||
# Content Management
|
|
||||||
MARBLE_WORKSPACE_KEY="workspace-key"
|
|
||||||
NEXT_PUBLIC_MARBLE_API_URL="https://api.marblecms.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Docker services:**
|
|
||||||
```bash
|
|
||||||
# Start local database and Redis
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
**Monorepo layout:**
|
|
||||||
- `apps/web/` - Main Next.js application
|
|
||||||
- `apps/desktop/` - Tauri desktop application
|
|
||||||
- `apps/bg-remover/` - Python background removal tools
|
|
||||||
- `apps/transcription/` - Audio transcription service
|
|
||||||
- `packages/` - Shared packages (auth, database)
|
|
||||||
|
|
||||||
**Web app structure:**
|
|
||||||
- `src/components/` - React components organized by feature
|
|
||||||
- `src/stores/` - Zustand state management
|
|
||||||
- `src/hooks/` - Custom React hooks
|
|
||||||
- `src/lib/` - Utility functions and services
|
|
||||||
- `src/types/` - TypeScript type definitions
|
|
||||||
- `src/app/` - Next.js app router pages and API routes
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
**Error handling:**
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
const result = await processData();
|
|
||||||
return { success: true, data: result };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Operation failed:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Store usage:**
|
|
||||||
```typescript
|
|
||||||
const { tracks, addTrack, updateTrack } = useTimelineStore();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Media processing:**
|
|
||||||
```typescript
|
|
||||||
import { processVideo } from '@/lib/ffmpeg-utils';
|
|
||||||
const processedVideo = await processVideo(inputFile, options);
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = [
|
||||||
|
'apps/desktop',
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
gpui = "0.2.2"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright 2025 OpenCut
|
Copyright 2026 OpenCut
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
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,177 +1,64 @@
|
|||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" width="120">
|
<td align="left" width="120">
|
||||||
<img src="apps/web/public/logo.png" alt="OpenCut Logo" width="100" />
|
<img src="https://assets.opencut.app/branding/symbol.svg" alt="OpenCut Logo" width="100" />
|
||||||
</td>
|
</td>
|
||||||
<td align="right">
|
<td align="right">
|
||||||
<h1>OpenCut</span></h1>
|
<h1>OpenCut</h1>
|
||||||
<h3 style="margin-top: -10px;">A free, open-source video editor for web, desktop, and mobile.</h3>
|
<h3 style="margin-top: -10px;">A free and open source video editor for web, desktop, and mobile.</h3>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Why?
|
[](https://discord.gg/zmR9N35cjK)
|
||||||
|
[](https://x.com/opencutapp)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
- **Privacy**: Your videos stay on your device
|
## Status
|
||||||
- **Free features**: Every basic feature of CapCut is paywalled now
|
|
||||||
- **Simple**: People want editors that are easy to use - CapCut proved that
|
|
||||||
|
|
||||||
## Features
|
**OpenCut is being rewritten from the ground up.** What's coming:
|
||||||
|
|
||||||
- Timeline-based editing
|
- An Editor API
|
||||||
- Multi-track support
|
- First-class third party plugins (made possible by a plugin-first architecture)
|
||||||
- Real-time preview
|
- Desktop, mobile, and browser from one codebase (Rust core)
|
||||||
- No watermarks or subscriptions
|
- MCP server (for AI agents)
|
||||||
- Analytics provided by [Databuddy](https://www.databuddy.cc?utm_source=opencut), 100% Anonymized & Non-invasive.
|
- Headless mode (automation, batch rendering)
|
||||||
- Blog powered by [Marble](https://marblecms.com?utm_source=opencut), Headless CMS.
|
- A scripting tab directly in the editor
|
||||||
|
|
||||||
## Project Structure
|
You can still find the previous version at [opencut-app/opencut-classic](https://github.com/opencut-app/opencut-classic), which is the one to reach for today. [opencut.app](https://opencut.app) still runs the classic version. The rewrite will live at [new.opencut.app](https://new.opencut.app) until it's ready to take over.
|
||||||
|
|
||||||
- `apps/web/` – Main Next.js web application
|
## Development
|
||||||
- `src/components/` – UI and editor components
|
|
||||||
- `src/hooks/` – Custom React hooks
|
|
||||||
- `src/lib/` – Utility and API logic
|
|
||||||
- `src/stores/` – State management (Zustand, etc.)
|
|
||||||
- `src/types/` – TypeScript types
|
|
||||||
|
|
||||||
## Getting Started
|
Install [proto](https://moonrepo.dev/proto) if you haven't already:
|
||||||
|
|
||||||
### Prerequisites
|
```sh
|
||||||
|
bash <(curl -fsSL https://moonrepo.dev/install/proto.sh)
|
||||||
Before you begin, ensure you have the following installed on your system:
|
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/en/) (v18 or later)
|
|
||||||
- [Bun](https://bun.sh/docs/installation)
|
|
||||||
(for `npm` alternative)
|
|
||||||
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
|
|
||||||
|
|
||||||
> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to run the frontend or want to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Clone your fork locally
|
|
||||||
3. Navigate to the web app directory: `cd apps/web`
|
|
||||||
4. Copy `.env.example` to `.env.local`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unix/Linux/Mac
|
|
||||||
cp .env.example .env.local
|
|
||||||
|
|
||||||
# Windows Command Prompt
|
|
||||||
copy .env.example .env.local
|
|
||||||
|
|
||||||
# Windows PowerShell
|
|
||||||
Copy-Item .env.example .env.local
|
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Install dependencies: `bun install`
|
From the repo root:
|
||||||
6. Start the development server: `bun dev`
|
|
||||||
|
|
||||||
## Development Setup
|
```sh
|
||||||
|
proto use # installs the tools pinned in .prototools
|
||||||
### Local Development
|
|
||||||
|
|
||||||
1. Start the database and Redis services:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From project root
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Navigate to the web app directory:
|
```sh
|
||||||
|
moon run web:dev # localhost:5173
|
||||||
```bash
|
moon run api:dev # localhost:8787
|
||||||
cd apps/web
|
moon run desktop:dev # see apps/desktop/README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Copy `.env.example` to `.env.local`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unix/Linux/Mac
|
|
||||||
cp .env.example .env.local
|
|
||||||
|
|
||||||
# Windows Command Prompt
|
|
||||||
copy .env.example .env.local
|
|
||||||
|
|
||||||
# Windows PowerShell
|
|
||||||
Copy-Item .env.example .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Configure required environment variables in `.env.local`:
|
|
||||||
|
|
||||||
**Required Variables:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Database (matches docker-compose.yaml)
|
|
||||||
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
|
|
||||||
|
|
||||||
# Generate a secure secret for Better Auth
|
|
||||||
BETTER_AUTH_SECRET="your-generated-secret-here"
|
|
||||||
BETTER_AUTH_URL="http://localhost:3000"
|
|
||||||
|
|
||||||
# Redis (matches docker-compose.yaml)
|
|
||||||
UPSTASH_REDIS_REST_URL="http://localhost:8079"
|
|
||||||
UPSTASH_REDIS_REST_TOKEN="example_token"
|
|
||||||
|
|
||||||
# Marble Blog
|
|
||||||
MARBLE_WORKSPACE_KEY=cm6ytuq9x0000i803v0isidst # example organization key
|
|
||||||
NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
|
|
||||||
|
|
||||||
# Development
|
|
||||||
NODE_ENV="development"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Generate BETTER_AUTH_SECRET:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unix/Linux/Mac
|
|
||||||
openssl rand -base64 32
|
|
||||||
|
|
||||||
# Windows PowerShell (simple method)
|
|
||||||
[System.Web.Security.Membership]::GeneratePassword(32, 0)
|
|
||||||
|
|
||||||
# Cross-platform (using Node.js)
|
|
||||||
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
|
|
||||||
|
|
||||||
# Or use an online generator: https://generate-secret.vercel.app/32
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Run database migrations: `bun run db:migrate` from (inside apps/web)
|
|
||||||
6. Start the development server: `bun run dev` from (inside apps/web)
|
|
||||||
|
|
||||||
The application will be available at [http://localhost:3000](http://localhost:3000).
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
|
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).
|
||||||
|
|
||||||
**🎯 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
|
||||||
|
|
||||||
Thanks to [Vercel](https://vercel.com?utm_source=github-opencut&utm_campaign=oss) for their support of open-source software.
|
OpenCut is supported by companies that believe in open source creator tools.
|
||||||
|
|
||||||
<a href="https://vercel.com/oss">
|
- [**fal.ai**](https://fal.ai?utm_source=github-opencut&utm_campaign=oss): Generative image, video, and audio models all in one place.
|
||||||
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FOpenCut-app%2FOpenCut&project-name=opencut&repository-name=opencut)
|
Want your logo here? Reach out at [sponsor@opencut.app](mailto:sponsor@opencut.app).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT LICENSE](LICENSE)
|
[MIT](LICENSE)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||

|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
$schema: 'https://moonrepo.dev/schemas/project.json'
|
||||||
|
|
||||||
|
language: 'typescript'
|
||||||
|
layer: 'application'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
dev:
|
||||||
|
command: 'bun run dev'
|
||||||
|
options:
|
||||||
|
runInCI: false # dev server: skipped in CI, never cached
|
||||||
|
|
||||||
|
build:
|
||||||
|
command: 'bun run build'
|
||||||
|
inputs:
|
||||||
|
- 'src/**/*'
|
||||||
|
- 'wrangler.jsonc'
|
||||||
|
- 'package.json'
|
||||||
|
outputs:
|
||||||
|
- 'dist'
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
command: 'bun run deploy'
|
||||||
|
deps:
|
||||||
|
- '~:build'
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@opencut/api",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"build": "wrangler deploy --dry-run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"elysia": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "latest",
|
||||||
|
"wrangler": "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { CloudflareAdapter } from "elysia/adapter/cloudflare-worker";
|
||||||
|
|
||||||
|
export default new Elysia({ adapter: CloudflareAdapter })
|
||||||
|
.get("/", () => ({ status: "ok" }))
|
||||||
|
.get("/health", () => ({ healthy: true, timestamp: new Date().toISOString() }))
|
||||||
|
.post(
|
||||||
|
"/echo",
|
||||||
|
({ body }) => body,
|
||||||
|
{
|
||||||
|
body: t.Object({ message: t.String() }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// .compile() is required, it triggers AoT compilation at startup
|
||||||
|
.compile();
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "opencut-api",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
// Minimum date required for CloudflareAdapter AoT support
|
||||||
|
"compatibility_date": "2025-06-01"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "opencut-desktop"
|
||||||
|
description = "OpenCut desktop app, built with GPUI"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "opencut-desktop"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui = { workspace = true }
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# OpenCut Desktop
|
||||||
|
|
||||||
|
Built with [GPUI](https://www.gpui.rs).
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Very early. Right now this is just a window that opens.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Rust is pinned in `.prototools` at the repo root (`proto use` installs it).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
moon run desktop:dev # cargo run
|
||||||
|
moon run desktop:check # cargo check
|
||||||
|
moon run desktop:build # cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The first build compiles GPUI from source and takes a while. The root `Cargo.lock` is committed.
|
||||||
|
|
||||||
|
## Platform requirements
|
||||||
|
|
||||||
|
- **macOS**: Xcode command line tools (Metal renderer).
|
||||||
|
- **Windows**: no extra dependencies (Win32 + DirectWrite).
|
||||||
|
- **Linux**: renders via Vulkan (Blade), windows via Wayland or X11 (both enabled by default). System packages (Debian/Ubuntu names): `libvulkan1` + working Vulkan drivers, `libwayland-dev`, `libx11-xcb-dev`, `libxkbcommon-x11-dev`, `libfontconfig-dev`, plus a C toolchain and `cmake`.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
$schema: 'https://moonrepo.dev/schemas/project.json'
|
||||||
|
|
||||||
|
language: 'rust'
|
||||||
|
layer: 'application'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
dev:
|
||||||
|
command: 'cargo run'
|
||||||
|
options:
|
||||||
|
runInCI: false
|
||||||
|
|
||||||
|
check:
|
||||||
|
command: 'cargo check'
|
||||||
|
inputs:
|
||||||
|
- 'src/**/*'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- '/Cargo.toml'
|
||||||
|
- '/Cargo.lock'
|
||||||
|
|
||||||
|
build:
|
||||||
|
command: 'cargo build --release'
|
||||||
|
inputs:
|
||||||
|
- 'src/**/*'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- '/Cargo.toml'
|
||||||
|
- '/Cargo.lock'
|
||||||
|
outputs:
|
||||||
|
- '/target/release/opencut-desktop'
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use gpui::{
|
||||||
|
div, prelude::*, px, rgb, size, App, Application, Bounds, Context, SharedString,
|
||||||
|
TitlebarOptions, Window, WindowBounds, WindowOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Root {
|
||||||
|
status: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Root {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.size_full()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.bg(rgb(0x111111))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(div().text_xl().child("OpenCut"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(rgb(0x888888))
|
||||||
|
.child(self.status.clone()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
Application::new().run(|cx: &mut App| {
|
||||||
|
let bounds = Bounds::centered(None, size(px(960.), px(600.)), cx);
|
||||||
|
cx.open_window(
|
||||||
|
WindowOptions {
|
||||||
|
titlebar: Some(TitlebarOptions {
|
||||||
|
title: Some(SharedString::from("OpenCut")),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|_, cx| {
|
||||||
|
cx.new(|_| Root {
|
||||||
|
status: "desktop shell scaffold".into(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("failed to open the main window");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
Before you follow anything in this guide, please make sure you've followed the steps in the [README](../../README.md) (under "Optional: Auto-captions (Transcription) Setup").
|
|
||||||
|
|
||||||
Open your terminal and make sure you're in the `apps/transcription` directory.
|
|
||||||
|
|
||||||
1. Create virtual environment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv env
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Activate it
|
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
env\Scripts\activate
|
|
||||||
```
|
|
||||||
|
|
||||||
**macOS/Linux:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source env/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: if you're using VS Code/Cursor and you're seeing errors with the imports about the modules not being found,
|
|
||||||
> You might have to press CTRL + Shift + P -> Python: Select Interpreter -> Enter interpreter path -> Find -> env -> scripts -> python.exe
|
|
||||||
|
|
||||||
3. Install libraries/packages/whatever you wanna call them
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Make sure you have a Modal account. If you don't: [create one](https://modal.com/)
|
|
||||||
|
|
||||||
> If you don't know what Modal is: it allows us to process the actual audio and transcribe with Whisper by providing the infra to run Python code with a lot of RAM, generally affordable.
|
|
||||||
|
|
||||||
5. Once you've got a Modal accoumt, run this:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m modal setup
|
|
||||||
```
|
|
||||||
|
|
||||||
It's gonna open a browser so you can authenticate.
|
|
||||||
|
|
||||||
6. Test it if you want to make sure it actually works:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
modal run transcription.py
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Deploy the function!
|
|
||||||
|
|
||||||
```bash
|
|
||||||
modal deploy transcription.py
|
|
||||||
```
|
|
||||||
|
|
||||||
7. Set the required secrets in Modal
|
|
||||||
|
|
||||||
So the script we just deployed interacts with Cloudflare to do two things:
|
|
||||||
|
|
||||||
- Download the audio (so it can be transcribed with Whisper)
|
|
||||||
- Delete the file after processing (privacy)
|
|
||||||
|
|
||||||
To do those things, the script needs access to these environment variables:
|
|
||||||
```bash
|
|
||||||
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
||||||
R2_ACCESS_KEY_ID=your-access-key-id
|
|
||||||
R2_SECRET_ACCESS_KEY=your-secret-access-key
|
|
||||||
R2_BUCKET_NAME=opencut-transcription
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember, we set these earlier in `.env.local`.
|
|
||||||
|
|
||||||
So let's do it:
|
|
||||||
|
|
||||||
- Go to [Modal Secrets](https://modal.com/secrets/mazewinther/main)
|
|
||||||
- Click "Custom" and enter "opencut-r2-secrets" for the name.
|
|
||||||
- Now you can just click "Import .env" and copy/paste the 4 variables from your `.env.local` file. Copy and paste these only:
|
|
||||||
```bash
|
|
||||||
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
||||||
R2_ACCESS_KEY_ID=your-access-key-id
|
|
||||||
R2_SECRET_ACCESS_KEY=your-secret-access-key
|
|
||||||
R2_BUCKET_NAME=opencut-transcription
|
|
||||||
```
|
|
||||||
- Click "Done" and you should see some cool particles!
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
modal
|
|
||||||
openai-whisper
|
|
||||||
boto3
|
|
||||||
pydantic
|
|
||||||
cryptography
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import modal
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
app = modal.App("opencut-transcription")
|
|
||||||
|
|
||||||
class TranscribeRequest(BaseModel):
|
|
||||||
filename: str
|
|
||||||
language: str = "auto"
|
|
||||||
decryptionKey: str = None
|
|
||||||
iv: str = None
|
|
||||||
|
|
||||||
@app.function(
|
|
||||||
image=modal.Image.debian_slim()
|
|
||||||
.apt_install(["ffmpeg"])
|
|
||||||
.pip_install(["openai-whisper", "boto3", "fastapi[standard]", "pydantic", "cryptography"]),
|
|
||||||
gpu="A10G",
|
|
||||||
timeout=300, # 5m
|
|
||||||
secrets=[modal.Secret.from_name("opencut-r2-secrets")]
|
|
||||||
)
|
|
||||||
@modal.fastapi_endpoint(method="POST")
|
|
||||||
def transcribe_audio(request: TranscribeRequest):
|
|
||||||
import whisper
|
|
||||||
import boto3
|
|
||||||
import tempfile
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
try:
|
|
||||||
filename = request.filename
|
|
||||||
language = request.language
|
|
||||||
decryption_key = request.decryptionKey
|
|
||||||
iv = request.iv
|
|
||||||
|
|
||||||
if not filename:
|
|
||||||
return {
|
|
||||||
"error": "Missing filename parameter"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Initialize R2 client
|
|
||||||
s3_client = boto3.client(
|
|
||||||
's3',
|
|
||||||
endpoint_url=f'https://{os.environ["CLOUDFLARE_ACCOUNT_ID"]}.r2.cloudflarestorage.com',
|
|
||||||
aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"],
|
|
||||||
aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"],
|
|
||||||
region_name='auto'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create temporary file for audio
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
|
|
||||||
temp_path = temp_file.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Download audio from R2
|
|
||||||
s3_client.download_file(
|
|
||||||
os.environ["R2_BUCKET_NAME"],
|
|
||||||
filename,
|
|
||||||
temp_path
|
|
||||||
)
|
|
||||||
|
|
||||||
# If decryption key provided, decrypt the file directly (zero-knowledge)
|
|
||||||
if decryption_key and iv:
|
|
||||||
import base64
|
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
|
|
||||||
# Read the encrypted file
|
|
||||||
with open(temp_path, 'rb') as f:
|
|
||||||
encrypted_data = f.read()
|
|
||||||
|
|
||||||
# Decode the key and IV from base64
|
|
||||||
key_bytes = base64.b64decode(decryption_key)
|
|
||||||
iv_bytes = base64.b64decode(iv)
|
|
||||||
|
|
||||||
# Decrypt the data using AES-GCM
|
|
||||||
# Extract the tag (last 16 bytes) and ciphertext
|
|
||||||
tag = encrypted_data[-16:]
|
|
||||||
ciphertext = encrypted_data[:-16]
|
|
||||||
|
|
||||||
cipher = Cipher(
|
|
||||||
algorithms.AES(key_bytes),
|
|
||||||
modes.GCM(iv_bytes, tag),
|
|
||||||
backend=default_backend()
|
|
||||||
)
|
|
||||||
decryptor = cipher.decryptor()
|
|
||||||
decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
|
|
||||||
|
|
||||||
# Write decrypted audio back to temp file
|
|
||||||
with open(temp_path, 'wb') as f:
|
|
||||||
f.write(decrypted_data)
|
|
||||||
|
|
||||||
# Load Whisper model
|
|
||||||
model = whisper.load_model("base")
|
|
||||||
|
|
||||||
# Transcribe audio
|
|
||||||
if language == "auto":
|
|
||||||
result = model.transcribe(temp_path)
|
|
||||||
else:
|
|
||||||
result = model.transcribe(temp_path, language=language.lower())
|
|
||||||
|
|
||||||
# Delete audio file from R2 (cleanup)
|
|
||||||
s3_client.delete_object(
|
|
||||||
Bucket=os.environ["R2_BUCKET_NAME"],
|
|
||||||
Key=filename
|
|
||||||
)
|
|
||||||
|
|
||||||
# Adjust segment timing - Whisper is consistently late by ~500ms
|
|
||||||
adjusted_segments = []
|
|
||||||
for segment in result["segments"]:
|
|
||||||
adjusted_segment = segment.copy()
|
|
||||||
# Shift start/end times earlier by 500ms, don't go below 0
|
|
||||||
adjusted_segment["start"] = max(0, segment["start"] - 0.5)
|
|
||||||
adjusted_segment["end"] = max(0.5, segment["end"] - 0.5) # Ensure duration is at least 0.5s
|
|
||||||
adjusted_segments.append(adjusted_segment)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"text": result["text"],
|
|
||||||
"segments": adjusted_segments,
|
|
||||||
"language": result["language"]
|
|
||||||
}
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Clean up temporary file
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.unlink(temp_path)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
print(f"Transcription error: {str(e)}")
|
|
||||||
print(f"Traceback: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
# Return error response that matches expected format
|
|
||||||
return {
|
|
||||||
"error": str(e),
|
|
||||||
"text": "",
|
|
||||||
"segments": [],
|
|
||||||
"language": "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
@app.local_entrypoint()
|
|
||||||
def main():
|
|
||||||
# Test function - you can call this with modal run transcription.py
|
|
||||||
print("Transcription service is ready to deploy!")
|
|
||||||
print("Deploy with: modal deploy transcription.py")
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"projectName": "web",
|
||||||
|
"mode": "file-router",
|
||||||
|
"typescript": true,
|
||||||
|
"packageManager": "npm",
|
||||||
|
"includeExamples": false,
|
||||||
|
"tailwind": true,
|
||||||
|
"addOnOptions": {},
|
||||||
|
"envVarValues": {},
|
||||||
|
"git": false,
|
||||||
|
"install": true,
|
||||||
|
"routerOnly": false,
|
||||||
|
"version": 1,
|
||||||
|
"framework": "react",
|
||||||
|
"chosenAddOns": [
|
||||||
|
"cloudflare",
|
||||||
|
"shadcn"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Environment Variables Example
|
|
||||||
# Copy this file to .env.local and update the values as needed
|
|
||||||
|
|
||||||
DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
|
|
||||||
|
|
||||||
# Better Auth
|
|
||||||
NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000
|
|
||||||
BETTER_AUTH_SECRET=your-secret-key-here
|
|
||||||
|
|
||||||
# Development Environment
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
UPSTASH_REDIS_REST_URL=http://localhost:8079
|
|
||||||
UPSTASH_REDIS_REST_TOKEN=example_token
|
|
||||||
|
|
||||||
# Marble Blog
|
|
||||||
MARBLE_WORKSPACE_KEY=cm6ytuq9x0000i803v0isidst # example organization key
|
|
||||||
NEXT_PUBLIC_MARBLE_API_URL=https://api.marblecms.com
|
|
||||||
|
|
||||||
# Freesound (generate at https://freesound.org/apiv2/apply/)
|
|
||||||
FREESOUND_CLIENT_ID=...
|
|
||||||
FREESOUND_API_KEY=...
|
|
||||||
|
|
||||||
# Cloudflare R2 (for auto-captions/transcription)
|
|
||||||
# Get these from Cloudflare Dashboard > R2 > Manage R2 API tokens
|
|
||||||
CLOUDFLARE_ACCOUNT_ID=your-account-id
|
|
||||||
R2_ACCESS_KEY_ID=your-access-key-id
|
|
||||||
R2_SECRET_ACCESS_KEY=your-secret-access-key
|
|
||||||
R2_BUCKET_NAME=opencut-transcription
|
|
||||||
|
|
||||||
# Modal transcription endpoint (from modal deploy transcription.py)
|
|
||||||
MODAL_TRANSCRIPTION_URL=https://your-username--opencut-transcription-transcribe-audio.modal.run
|
|
||||||
@@ -1,2 +1,13 @@
|
|||||||
# Turborepo
|
node_modules
|
||||||
.turbo
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
.nitro
|
||||||
|
.tanstack
|
||||||
|
.wrangler
|
||||||
|
.output
|
||||||
|
.vinxi
|
||||||
|
__unconfig*
|
||||||
|
todos.json
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
},
|
||||||
|
"files.readonlyInclude": {
|
||||||
|
"**/routeTree.gen.ts": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
FROM oven/bun:alpine AS base
|
|
||||||
|
|
||||||
# Install dependencies and build the application
|
|
||||||
FROM base AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG FREESOUND_CLIENT_ID
|
|
||||||
ARG FREESOUND_API_KEY
|
|
||||||
|
|
||||||
COPY package.json package.json
|
|
||||||
COPY bun.lock bun.lock
|
|
||||||
COPY turbo.json turbo.json
|
|
||||||
|
|
||||||
COPY apps/web/package.json apps/web/package.json
|
|
||||||
COPY packages/db/package.json packages/db/package.json
|
|
||||||
COPY packages/auth/package.json packages/auth/package.json
|
|
||||||
|
|
||||||
RUN bun install
|
|
||||||
|
|
||||||
COPY apps/web/ apps/web/
|
|
||||||
COPY packages/db/ packages/db/
|
|
||||||
COPY packages/auth/ packages/auth/
|
|
||||||
|
|
||||||
ENV NODE_ENV production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
# Set build-time environment variables for validation
|
|
||||||
ENV DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
|
|
||||||
ENV BETTER_AUTH_SECRET="build-time-secret"
|
|
||||||
ENV UPSTASH_REDIS_REST_URL="http://localhost:8079"
|
|
||||||
ENV UPSTASH_REDIS_REST_TOKEN="example_token"
|
|
||||||
ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
|
|
||||||
|
|
||||||
ENV FREESOUND_CLIENT_ID=$FREESOUND_CLIENT_ID
|
|
||||||
ENV FREESOUND_API_KEY=$FREESOUND_API_KEY
|
|
||||||
|
|
||||||
WORKDIR /app/apps/web
|
|
||||||
RUN bun run build
|
|
||||||
|
|
||||||
# Production image
|
|
||||||
FROM base AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
|
|
||||||
|
|
||||||
RUN chown nextjs:nodejs apps
|
|
||||||
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENV PORT=3000
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
CMD ["bun", "apps/web/server.js"]
|
|
||||||
|
|
||||||
@@ -1,21 +1,25 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
"style": "new-york",
|
"style": "base-mira",
|
||||||
"rsc": true,
|
"rsc": false,
|
||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/app/globals.css",
|
"css": "src/styles.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"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide"
|
"menuColor": "default",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { Config } from "drizzle-kit";
|
|
||||||
import * as dotenv from "dotenv";
|
|
||||||
|
|
||||||
// Load the right env file based on environment
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
|
||||||
dotenv.config({ path: ".env.production" });
|
|
||||||
} else {
|
|
||||||
dotenv.config({ path: ".env.local" });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
schema: "../../packages/db/src/schema.ts",
|
|
||||||
dialect: "postgresql",
|
|
||||||
dbCredentials: {
|
|
||||||
url: process.env.DATABASE_URL!,
|
|
||||||
},
|
|
||||||
out: "./migrations",
|
|
||||||
strict: process.env.NODE_ENV === "production",
|
|
||||||
} satisfies Config;
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
CREATE TABLE "accounts" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"account_id" text NOT NULL,
|
|
||||||
"provider_id" text NOT NULL,
|
|
||||||
"user_id" text NOT NULL,
|
|
||||||
"access_token" text,
|
|
||||||
"refresh_token" text,
|
|
||||||
"id_token" text,
|
|
||||||
"access_token_expires_at" timestamp,
|
|
||||||
"refresh_token_expires_at" timestamp,
|
|
||||||
"scope" text,
|
|
||||||
"password" text,
|
|
||||||
"created_at" timestamp NOT NULL,
|
|
||||||
"updated_at" timestamp NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "accounts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
||||||
CREATE TABLE "sessions" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"expires_at" timestamp NOT NULL,
|
|
||||||
"token" text NOT NULL,
|
|
||||||
"created_at" timestamp NOT NULL,
|
|
||||||
"updated_at" timestamp NOT NULL,
|
|
||||||
"ip_address" text,
|
|
||||||
"user_agent" text,
|
|
||||||
"user_id" text NOT NULL,
|
|
||||||
CONSTRAINT "sessions_token_unique" UNIQUE("token")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
||||||
CREATE TABLE "users" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"email" text NOT NULL,
|
|
||||||
"email_verified" boolean NOT NULL,
|
|
||||||
"image" text,
|
|
||||||
"created_at" timestamp NOT NULL,
|
|
||||||
"updated_at" timestamp NOT NULL,
|
|
||||||
CONSTRAINT "users_email_unique" UNIQUE("email")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
||||||
CREATE TABLE "verifications" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"identifier" text NOT NULL,
|
|
||||||
"value" text NOT NULL,
|
|
||||||
"expires_at" timestamp NOT NULL,
|
|
||||||
"created_at" timestamp,
|
|
||||||
"updated_at" timestamp
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
||||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
CREATE TABLE "waitlist" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"email" text NOT NULL,
|
|
||||||
"created_at" timestamp NOT NULL,
|
|
||||||
CONSTRAINT "waitlist_email_unique" UNIQUE("email")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "users" ALTER COLUMN "email_verified" SET DEFAULT false;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE "export_waitlist" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"email" text NOT NULL,
|
|
||||||
"created_at" timestamp NOT NULL,
|
|
||||||
"updated_at" timestamp NOT NULL,
|
|
||||||
CONSTRAINT "export_waitlist_email_unique" UNIQUE("email")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "export_waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
||||||
DROP TABLE "waitlist" CASCADE;
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e",
|
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.accounts": {
|
|
||||||
"name": "accounts",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"account_id": {
|
|
||||||
"name": "account_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"provider_id": {
|
|
||||||
"name": "provider_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"access_token": {
|
|
||||||
"name": "access_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token": {
|
|
||||||
"name": "refresh_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"id_token": {
|
|
||||||
"name": "id_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"access_token_expires_at": {
|
|
||||||
"name": "access_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token_expires_at": {
|
|
||||||
"name": "refresh_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"scope": {
|
|
||||||
"name": "scope",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"accounts_user_id_users_id_fk": {
|
|
||||||
"name": "accounts_user_id_users_id_fk",
|
|
||||||
"tableFrom": "accounts",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.sessions": {
|
|
||||||
"name": "sessions",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"ip_address": {
|
|
||||||
"name": "ip_address",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_agent": {
|
|
||||||
"name": "user_agent",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"sessions_user_id_users_id_fk": {
|
|
||||||
"name": "sessions_user_id_users_id_fk",
|
|
||||||
"tableFrom": "sessions",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"sessions_token_unique": {
|
|
||||||
"name": "sessions_token_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["token"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email_verified": {
|
|
||||||
"name": "email_verified",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"users_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.verifications": {
|
|
||||||
"name": "verifications",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"identifier": {
|
|
||||||
"name": "identifier",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f",
|
|
||||||
"prevId": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.accounts": {
|
|
||||||
"name": "accounts",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"account_id": {
|
|
||||||
"name": "account_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"provider_id": {
|
|
||||||
"name": "provider_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"access_token": {
|
|
||||||
"name": "access_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token": {
|
|
||||||
"name": "refresh_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"id_token": {
|
|
||||||
"name": "id_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"access_token_expires_at": {
|
|
||||||
"name": "access_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token_expires_at": {
|
|
||||||
"name": "refresh_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"scope": {
|
|
||||||
"name": "scope",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"accounts_user_id_users_id_fk": {
|
|
||||||
"name": "accounts_user_id_users_id_fk",
|
|
||||||
"tableFrom": "accounts",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.sessions": {
|
|
||||||
"name": "sessions",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"ip_address": {
|
|
||||||
"name": "ip_address",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_agent": {
|
|
||||||
"name": "user_agent",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"sessions_user_id_users_id_fk": {
|
|
||||||
"name": "sessions_user_id_users_id_fk",
|
|
||||||
"tableFrom": "sessions",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"sessions_token_unique": {
|
|
||||||
"name": "sessions_token_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["token"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email_verified": {
|
|
||||||
"name": "email_verified",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"users_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.verifications": {
|
|
||||||
"name": "verifications",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"identifier": {
|
|
||||||
"name": "identifier",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.waitlist": {
|
|
||||||
"name": "waitlist",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"waitlist_email_unique": {
|
|
||||||
"name": "waitlist_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "1b9148ed-497b-4e53-8cf6-f556c8fe7f7f",
|
|
||||||
"prevId": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.accounts": {
|
|
||||||
"name": "accounts",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"account_id": {
|
|
||||||
"name": "account_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"provider_id": {
|
|
||||||
"name": "provider_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"access_token": {
|
|
||||||
"name": "access_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token": {
|
|
||||||
"name": "refresh_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"id_token": {
|
|
||||||
"name": "id_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"access_token_expires_at": {
|
|
||||||
"name": "access_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token_expires_at": {
|
|
||||||
"name": "refresh_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"scope": {
|
|
||||||
"name": "scope",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"accounts_user_id_users_id_fk": {
|
|
||||||
"name": "accounts_user_id_users_id_fk",
|
|
||||||
"tableFrom": "accounts",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.sessions": {
|
|
||||||
"name": "sessions",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"ip_address": {
|
|
||||||
"name": "ip_address",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_agent": {
|
|
||||||
"name": "user_agent",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"sessions_user_id_users_id_fk": {
|
|
||||||
"name": "sessions_user_id_users_id_fk",
|
|
||||||
"tableFrom": "sessions",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": ["user_id"],
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"sessions_token_unique": {
|
|
||||||
"name": "sessions_token_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["token"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email_verified": {
|
|
||||||
"name": "email_verified",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"users_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.verifications": {
|
|
||||||
"name": "verifications",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"identifier": {
|
|
||||||
"name": "identifier",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.waitlist": {
|
|
||||||
"name": "waitlist",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"waitlist_email_unique": {
|
|
||||||
"name": "waitlist_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": ["email"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "71b84015-78f8-4d07-9115-8d56ea550459",
|
|
||||||
"prevId": "1b9148ed-497b-4e53-8cf6-f556c8fe7f7f",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.accounts": {
|
|
||||||
"name": "accounts",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"account_id": {
|
|
||||||
"name": "account_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"provider_id": {
|
|
||||||
"name": "provider_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"access_token": {
|
|
||||||
"name": "access_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token": {
|
|
||||||
"name": "refresh_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"id_token": {
|
|
||||||
"name": "id_token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"access_token_expires_at": {
|
|
||||||
"name": "access_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"refresh_token_expires_at": {
|
|
||||||
"name": "refresh_token_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"scope": {
|
|
||||||
"name": "scope",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"accounts_user_id_users_id_fk": {
|
|
||||||
"name": "accounts_user_id_users_id_fk",
|
|
||||||
"tableFrom": "accounts",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.export_waitlist": {
|
|
||||||
"name": "export_waitlist",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"export_waitlist_email_unique": {
|
|
||||||
"name": "export_waitlist_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.sessions": {
|
|
||||||
"name": "sessions",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"ip_address": {
|
|
||||||
"name": "ip_address",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_agent": {
|
|
||||||
"name": "user_agent",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"sessions_user_id_users_id_fk": {
|
|
||||||
"name": "sessions_user_id_users_id_fk",
|
|
||||||
"tableFrom": "sessions",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"sessions_token_unique": {
|
|
||||||
"name": "sessions_token_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email_verified": {
|
|
||||||
"name": "email_verified",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"name": "image",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"users_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
},
|
|
||||||
"public.verifications": {
|
|
||||||
"name": "verifications",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"identifier": {
|
|
||||||
"name": "identifier",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"name": "value",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"expires_at": {
|
|
||||||
"name": "expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1750581188229,
|
|
||||||
"tag": "0000_hot_the_fallen",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1750689835736,
|
|
||||||
"tag": "0001_tricky_jackpot",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1752569400809,
|
|
||||||
"tag": "0002_cuddly_pretty_boy",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1755377469662,
|
|
||||||
"tag": "0003_long_polaris",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
$schema: 'https://moonrepo.dev/schemas/project.json'
|
||||||
|
|
||||||
|
language: 'typescript'
|
||||||
|
layer: 'application'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
dev:
|
||||||
|
command: 'bun run dev'
|
||||||
|
options:
|
||||||
|
runInCI: false # dev server: skipped in CI, never cached
|
||||||
|
|
||||||
|
build:
|
||||||
|
command: 'bun run build'
|
||||||
|
inputs:
|
||||||
|
- 'src/**/*'
|
||||||
|
- 'public/**/*'
|
||||||
|
- 'vite.config.ts'
|
||||||
|
- 'tsconfig.json'
|
||||||
|
- 'package.json'
|
||||||
|
outputs:
|
||||||
|
- 'dist'
|
||||||
|
|
||||||
|
test:
|
||||||
|
command: 'bun run test'
|
||||||
|
inputs:
|
||||||
|
- 'src/**/*'
|
||||||
|
- 'tsconfig.json'
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
command: 'bun run deploy'
|
||||||
|
deps:
|
||||||
|
- '~:build'
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
import { withBotId } from "botid/next/config";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
compiler: {
|
|
||||||
removeConsole: process.env.NODE_ENV === "production",
|
|
||||||
},
|
|
||||||
reactStrictMode: true,
|
|
||||||
productionBrowserSourceMaps: true,
|
|
||||||
output: "standalone",
|
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "plus.unsplash.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "images.unsplash.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "images.marblecms.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "lh3.googleusercontent.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "avatars.githubusercontent.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "api.iconify.design",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "api.simplesvg.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "api.unisvg.com",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withBotId(nextConfig);
|
|
||||||
@@ -1,89 +1,73 @@
|
|||||||
{
|
{
|
||||||
"name": "opencut",
|
"name": "@opencut/web",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1.2.18",
|
"type": "module",
|
||||||
|
"imports": {
|
||||||
|
"#/*": "./src/*"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "vite dev --port 5173",
|
||||||
"build": "next build",
|
"build": "vite build",
|
||||||
"start": "next start",
|
"preview": "vite preview",
|
||||||
"lint": "biome check src/",
|
"test": "vitest run",
|
||||||
"lint:fix": "biome check src/ --write",
|
"deploy": "bun run build && wrangler deploy"
|
||||||
"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": {
|
||||||
"@ffmpeg/core": "^0.12.10",
|
"@base-ui/react": "^1.4.1",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@cloudflare/vite-plugin": "^1.26.0",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hugeicons/core-free-icons": "^4.1.2",
|
||||||
"@opencut/auth": "workspace:*",
|
"@hugeicons/react": "^1.1.6",
|
||||||
"@opencut/db": "workspace:*",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@tanstack/react-devtools": "latest",
|
||||||
"@t3-oss/env-core": "^0.13.8",
|
"@tanstack/react-router": "latest",
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@tanstack/react-router-devtools": "latest",
|
||||||
"@upstash/ratelimit": "^2.0.5",
|
"@tanstack/react-router-ssr-query": "latest",
|
||||||
"@upstash/redis": "^1.35.0",
|
"@tanstack/react-start": "latest",
|
||||||
"@vercel/analytics": "^1.4.1",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"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.0.0",
|
"cmdk": "^1.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.5.0",
|
"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",
|
||||||
"feed": "^5.1.0",
|
"next-themes": "^0.4.6",
|
||||||
"framer-motion": "^11.13.1",
|
"radix-ui": "^1.4.3",
|
||||||
"input-otp": "^1.4.1",
|
"react": "^19.2.0",
|
||||||
"lucide-react": "^0.468.0",
|
"react-day-picker": "^10.0.0",
|
||||||
"motion": "^12.18.1",
|
"react-dom": "^19.2.0",
|
||||||
"nanoid": "^5.1.5",
|
"react-hook-form": "^7.75.0",
|
||||||
"next": "^15.4.5",
|
"react-resizable-panels": "^4.11.0",
|
||||||
"next-themes": "^0.4.4",
|
"recharts": "3.8.0",
|
||||||
"pg": "^8.16.2",
|
"shadcn": "^4.7.0",
|
||||||
"radix-ui": "^1.4.2",
|
"sonner": "^2.0.7",
|
||||||
"react": "^18.2.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"react-day-picker": "^8.10.1",
|
"tailwindcss": "^4.1.18",
|
||||||
"react-dom": "^18.2.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"react-hook-form": "^7.54.0",
|
"vaul": "^1.1.2",
|
||||||
"react-icons": "^5.4.0",
|
"zod": "^4.4.3"
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"react-phone-number-input": "^3.4.11",
|
|
||||||
"react-resizable-panels": "^2.1.7",
|
|
||||||
"recharts": "^2.14.1",
|
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
|
||||||
"rehype-parse": "^9.0.1",
|
|
||||||
"rehype-sanitize": "^6.0.0",
|
|
||||||
"rehype-slug": "^6.0.0",
|
|
||||||
"rehype-stringify": "^10.0.1",
|
|
||||||
"sonner": "^1.7.1",
|
|
||||||
"tailwind-merge": "^2.5.5",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"unified": "^11.0.5",
|
|
||||||
"vaul": "^1.1.1",
|
|
||||||
"zod": "^3.25.67",
|
|
||||||
"zustand": "^5.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/bun": "latest",
|
"@tanstack/devtools-vite": "latest",
|
||||||
"@types/node": "^24.2.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/pg": "^8.15.4",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/react": "^18.2.48",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react": "^19.2.0",
|
||||||
"cross-env": "^7.0.3",
|
"@types/react-dom": "^19.2.0",
|
||||||
"drizzle-kit": "^0.31.4",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"postcss": "^8",
|
"jsdom": "^28.1.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"typescript": "^6.0.2",
|
||||||
"tsx": "^4.7.1",
|
"vite": "^8.0.0",
|
||||||
"typescript": "^5.8.3"
|
"vitest": "^4.1.5",
|
||||||
|
"wrangler": "^4.70.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild",
|
||||||
|
"lightningcss"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square70x70logo src="/icons/ms-icon-70x70.png"/>
|
|
||||||
<square150x150logo src="/icons/ms-icon-150x150.png"/>
|
|
||||||
<square310x310logo src="/icons/ms-icon-310x310.png"/>
|
|
||||||
<TileColor>#ffffff</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 195 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<svg width="459" height="77" viewBox="0 0 459 77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect x="0.5" y="1.15625" width="458" height="75" rx="15.5" fill="#101010"/>
|
|
||||||
<rect x="0.5" y="1.15625" width="458" height="75" rx="15.5" stroke="#FFCC00"/>
|
|
||||||
<rect x="0.5" y="1.15625" width="31" height="75" rx="15.5" fill="#2A2A2A"/>
|
|
||||||
<rect x="0.5" y="1.15625" width="31" height="75" rx="15.5" stroke="#FFCC00"/>
|
|
||||||
<rect x="13" y="20.6562" width="6" height="36" rx="3" fill="#FFCC00"/>
|
|
||||||
<rect x="427.5" y="1.15625" width="31" height="75" rx="15.5" fill="#2A2A2A"/>
|
|
||||||
<rect x="427.5" y="1.15625" width="31" height="75" rx="15.5" stroke="#FFCC00"/>
|
|
||||||
<rect x="440" y="20.6562" width="6" height="36" rx="3" fill="#FFCC00"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 716 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 741 B |
|
Before Width: | Height: | Size: 768 B |
|
Before Width: | Height: | Size: 802 B |
|
Before Width: | Height: | Size: 826 B |
|
Before Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 998 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 809 B |
|
Before Width: | Height: | Size: 843 B |
|
Before Width: | Height: | Size: 826 B |
|
Before Width: | Height: | Size: 820 B |
|
Before Width: | Height: | Size: 670 B |
|
Before Width: | Height: | Size: 747 B |
|
Before Width: | Height: | Size: 906 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 814 B |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_10_2)">
|
|
||||||
<path d="M32 9.37305V22.627L22.627 32H9.37305L0 22.627V9.37305L9.37305 0H22.627L32 9.37305ZM8 8V24H24V8H8Z" fill="white"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_10_2">
|
|
||||||
<rect width="32" height="32" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 362 B |
@@ -1,44 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "OpenCut",
|
"short_name": "TanStack App",
|
||||||
"description": "A simple but powerful video editor that gets the job done. In your browser.",
|
"name": "Create TanStack App Sample",
|
||||||
"display": "standalone",
|
|
||||||
"start_url": "/",
|
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-36x36.png",
|
"src": "favicon.ico",
|
||||||
"sizes": "36x36",
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
"type": "image\/png",
|
"type": "image/x-icon"
|
||||||
"density": "0.75"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-48x48.png",
|
"src": "logo192.png",
|
||||||
"sizes": "48x48",
|
"type": "image/png",
|
||||||
"type": "image\/png",
|
"sizes": "192x192"
|
||||||
"density": "1.0"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/android-icon-72x72.png",
|
"src": "logo512.png",
|
||||||
"sizes": "72x72",
|
"type": "image/png",
|
||||||
"type": "image\/png",
|
"sizes": "512x512"
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { memo, Suspense } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { ArrowLeft, Loader2 } from "lucide-react";
|
|
||||||
import { GoogleIcon } from "@/components/icons";
|
|
||||||
import { useLogin } from "@/hooks/auth/useLogin";
|
|
||||||
|
|
||||||
const LoginPage = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const {
|
|
||||||
email,
|
|
||||||
setEmail,
|
|
||||||
password,
|
|
||||||
setPassword,
|
|
||||||
error,
|
|
||||||
isAnyLoading,
|
|
||||||
isEmailLoading,
|
|
||||||
isGoogleLoading,
|
|
||||||
handleLogin,
|
|
||||||
handleGoogleLogin,
|
|
||||||
} = useLogin();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen items-center justify-center relative">
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
className="absolute top-6 left-6"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-5 w-5" /> Back
|
|
||||||
</Button>
|
|
||||||
<Card className="w-[400px] shadow-lg border-0">
|
|
||||||
<CardHeader className="text-center pb-4">
|
|
||||||
<CardTitle className="text-2xl font-semibold">Welcome back</CardTitle>
|
|
||||||
<CardDescription className="text-base">
|
|
||||||
Sign in to your account to continue
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="text-center">
|
|
||||||
<Loader2 className="animate-spin" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-6">
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleGoogleLogin}
|
|
||||||
variant="outline"
|
|
||||||
size="lg"
|
|
||||||
disabled={isAnyLoading}
|
|
||||||
>
|
|
||||||
{isGoogleLoading ? (
|
|
||||||
<Loader2 className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<GoogleIcon />
|
|
||||||
)}{" "}
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<Separator className="w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
|
||||||
<span className="bg-background px-2 text-muted-foreground">
|
|
||||||
Or continue with
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="m@example.com"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={isAnyLoading}
|
|
||||||
className="h-11"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Password</Label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
disabled={isAnyLoading}
|
|
||||||
className="h-11"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleLogin}
|
|
||||||
disabled={isAnyLoading || !email || !password}
|
|
||||||
className="w-full h-11"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{isEmailLoading ? (
|
|
||||||
<Loader2 className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Sign in"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 text-center text-sm">
|
|
||||||
Don't have an account?{" "}
|
|
||||||
<Link
|
|
||||||
href="/signup"
|
|
||||||
className="font-medium text-primary underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
Sign up
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(LoginPage);
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { memo, Suspense } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { ArrowLeft, Loader2 } from "lucide-react";
|
|
||||||
import { GoogleIcon } from "@/components/icons";
|
|
||||||
import { useSignUp } from "@/hooks/auth/useSignUp";
|
|
||||||
|
|
||||||
const SignUpPage = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
setName,
|
|
||||||
email,
|
|
||||||
setEmail,
|
|
||||||
password,
|
|
||||||
setPassword,
|
|
||||||
error,
|
|
||||||
isAnyLoading,
|
|
||||||
isEmailLoading,
|
|
||||||
isGoogleLoading,
|
|
||||||
handleSignUp,
|
|
||||||
handleGoogleSignUp,
|
|
||||||
} = useSignUp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen items-center justify-center relative">
|
|
||||||
<Button
|
|
||||||
variant="text"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
className="absolute top-6 left-6"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-5 w-5" /> Back
|
|
||||||
</Button>
|
|
||||||
<Card className="w-[400px] shadow-lg border-0">
|
|
||||||
<CardHeader className="text-center pb-4">
|
|
||||||
<CardTitle className="text-2xl font-semibold">
|
|
||||||
Create your account
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-base">
|
|
||||||
Get started with your free account today
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="text-center">
|
|
||||||
<Loader2 className="animate-spin" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-6">
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
onClick={handleGoogleSignUp}
|
|
||||||
variant="outline"
|
|
||||||
size="lg"
|
|
||||||
disabled={isAnyLoading}
|
|
||||||
>
|
|
||||||
{isGoogleLoading ? (
|
|
||||||
<Loader2 className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
<GoogleIcon />
|
|
||||||
)}{" "}
|
|
||||||
Continue with Google
|
|
||||||
</Button>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<Separator className="w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
|
||||||
<span className="bg-background px-2 text-muted-foreground">
|
|
||||||
Or continue with
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Full Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
type="text"
|
|
||||||
placeholder="John Doe"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
disabled={isAnyLoading}
|
|
||||||
className="h-11"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="m@example.com"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={isAnyLoading}
|
|
||||||
className="h-11"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Password</Label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Create a strong password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
disabled={isAnyLoading}
|
|
||||||
className="h-11"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleSignUp}
|
|
||||||
disabled={isAnyLoading || !name || !email || !password}
|
|
||||||
className="w-full h-11"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
{isEmailLoading ? (
|
|
||||||
<Loader2 className="animate-spin" />
|
|
||||||
) : (
|
|
||||||
"Create account"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 text-center text-sm">
|
|
||||||
Already have an account?{" "}
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className="font-medium text-primary underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(SignUpPage);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { InputWithBack } from "@/components/ui/input-with-back";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export default function AnimationPage() {
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-dvh flex">
|
|
||||||
<div className="bg-panel p-6 w-[20rem]">
|
|
||||||
<InputWithBack isExpanded={isExpanded} setIsExpanded={setIsExpanded} />
|
|
||||||
</div>
|
|
||||||
<div className="p-6 flex items-end justify-end">
|
|
||||||
<p
|
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
|
||||||
className="cursor-pointer hover:opacity-75 transition-opacity"
|
|
||||||
>
|
|
||||||
{isExpanded ? "Collapse" : "Expand"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { auth } from "@opencut/auth";
|
|
||||||
import { toNextJsHandler } from "better-auth/next-js";
|
|
||||||
|
|
||||||
export const { POST, GET } = toNextJsHandler(auth);
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { AwsClient } from "aws4fetch";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { env } from "@/env";
|
|
||||||
import { baseRateLimit } from "@/lib/rate-limit";
|
|
||||||
import { isTranscriptionConfigured } from "@/lib/transcription-utils";
|
|
||||||
|
|
||||||
const uploadRequestSchema = z.object({
|
|
||||||
fileExtension: z.enum(["wav", "mp3", "m4a", "flac"], {
|
|
||||||
errorMap: () => ({
|
|
||||||
message: "File extension must be wav, mp3, m4a, or flac",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiResponseSchema = z.object({
|
|
||||||
uploadUrl: z.string().url(),
|
|
||||||
fileName: z.string().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Rate limiting
|
|
||||||
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
|
|
||||||
const { success } = await baseRateLimit.limit(ip);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check transcription configuration
|
|
||||||
const transcriptionCheck = isTranscriptionConfigured();
|
|
||||||
if (!transcriptionCheck.configured) {
|
|
||||||
console.error(
|
|
||||||
"Missing environment variables:",
|
|
||||||
JSON.stringify(transcriptionCheck.missingVars)
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Transcription not configured",
|
|
||||||
message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
|
|
||||||
},
|
|
||||||
{ status: 503 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and validate request body
|
|
||||||
const rawBody = await request.json().catch(() => null);
|
|
||||||
if (!rawBody) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid JSON in request body" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationResult = uploadRequestSchema.safeParse(rawBody);
|
|
||||||
if (!validationResult.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Invalid request parameters",
|
|
||||||
details: validationResult.error.flatten().fieldErrors,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { fileExtension } = validationResult.data;
|
|
||||||
|
|
||||||
// Initialize R2 client
|
|
||||||
const client = new AwsClient({
|
|
||||||
accessKeyId: env.R2_ACCESS_KEY_ID,
|
|
||||||
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate unique filename with timestamp
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const fileName = `audio/${timestamp}-${nanoid()}.${fileExtension}`;
|
|
||||||
|
|
||||||
// Create presigned URL
|
|
||||||
const url = new URL(
|
|
||||||
`https://${env.R2_BUCKET_NAME}.${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}`
|
|
||||||
);
|
|
||||||
|
|
||||||
url.searchParams.set("X-Amz-Expires", "3600"); // 1 hour expiry
|
|
||||||
|
|
||||||
const signed = await client.sign(new Request(url, { method: "PUT" }), {
|
|
||||||
aws: { signQuery: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!signed.url) {
|
|
||||||
throw new Error("Failed to generate presigned URL");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare and validate response
|
|
||||||
const responseData = {
|
|
||||||
uploadUrl: signed.url,
|
|
||||||
fileName,
|
|
||||||
};
|
|
||||||
|
|
||||||
const responseValidation = apiResponseSchema.safeParse(responseData);
|
|
||||||
if (!responseValidation.success) {
|
|
||||||
console.error(
|
|
||||||
"Invalid API response structure:",
|
|
||||||
responseValidation.error
|
|
||||||
);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal response formatting error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(responseValidation.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error generating upload URL:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Failed to generate upload URL",
|
|
||||||
message:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "An unexpected error occurred",
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { NextRequest } from "next/server";
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
return new Response("OK", { status: 200 });
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { env } from "@/env";
|
|
||||||
import { baseRateLimit } from "@/lib/rate-limit";
|
|
||||||
|
|
||||||
const searchParamsSchema = z.object({
|
|
||||||
q: z.string().max(500, "Query too long").optional(),
|
|
||||||
type: z.enum(["songs", "effects"]).optional(),
|
|
||||||
page: z.coerce.number().int().min(1).max(1000).default(1),
|
|
||||||
page_size: z.coerce.number().int().min(1).max(150).default(20),
|
|
||||||
sort: z
|
|
||||||
.enum(["downloads", "rating", "created", "score"])
|
|
||||||
.default("downloads"),
|
|
||||||
min_rating: z.coerce.number().min(0).max(5).default(3),
|
|
||||||
commercial_only: z.coerce.boolean().default(true),
|
|
||||||
});
|
|
||||||
|
|
||||||
const freesoundResultSchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
name: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
url: z.string().url(),
|
|
||||||
previews: z
|
|
||||||
.object({
|
|
||||||
"preview-hq-mp3": z.string().url(),
|
|
||||||
"preview-lq-mp3": z.string().url(),
|
|
||||||
"preview-hq-ogg": z.string().url(),
|
|
||||||
"preview-lq-ogg": z.string().url(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
download: z.string().url().optional(),
|
|
||||||
duration: z.number(),
|
|
||||||
filesize: z.number(),
|
|
||||||
type: z.string(),
|
|
||||||
channels: z.number(),
|
|
||||||
bitrate: z.number(),
|
|
||||||
bitdepth: z.number(),
|
|
||||||
samplerate: z.number(),
|
|
||||||
username: z.string(),
|
|
||||||
tags: z.array(z.string()),
|
|
||||||
license: z.string(),
|
|
||||||
created: z.string(),
|
|
||||||
num_downloads: z.number().optional(),
|
|
||||||
avg_rating: z.number().optional(),
|
|
||||||
num_ratings: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const freesoundResponseSchema = z.object({
|
|
||||||
count: z.number(),
|
|
||||||
next: z.string().url().nullable(),
|
|
||||||
previous: z.string().url().nullable(),
|
|
||||||
results: z.array(freesoundResultSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const transformedResultSchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
name: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
url: z.string(),
|
|
||||||
previewUrl: z.string().optional(),
|
|
||||||
downloadUrl: z.string().optional(),
|
|
||||||
duration: z.number(),
|
|
||||||
filesize: z.number(),
|
|
||||||
type: z.string(),
|
|
||||||
channels: z.number(),
|
|
||||||
bitrate: z.number(),
|
|
||||||
bitdepth: z.number(),
|
|
||||||
samplerate: z.number(),
|
|
||||||
username: z.string(),
|
|
||||||
tags: z.array(z.string()),
|
|
||||||
license: z.string(),
|
|
||||||
created: z.string(),
|
|
||||||
downloads: z.number().optional(),
|
|
||||||
rating: z.number().optional(),
|
|
||||||
ratingCount: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiResponseSchema = z.object({
|
|
||||||
count: z.number(),
|
|
||||||
next: z.string().nullable(),
|
|
||||||
previous: z.string().nullable(),
|
|
||||||
results: z.array(transformedResultSchema),
|
|
||||||
query: z.string().optional(),
|
|
||||||
type: z.string(),
|
|
||||||
page: z.number(),
|
|
||||||
pageSize: z.number(),
|
|
||||||
sort: z.string(),
|
|
||||||
minRating: z.number().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
|
|
||||||
const { success } = await baseRateLimit.limit(ip);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
|
|
||||||
const validationResult = searchParamsSchema.safeParse({
|
|
||||||
q: searchParams.get("q") || undefined,
|
|
||||||
type: searchParams.get("type") || undefined,
|
|
||||||
page: searchParams.get("page") || undefined,
|
|
||||||
page_size: searchParams.get("page_size") || undefined,
|
|
||||||
sort: searchParams.get("sort") || undefined,
|
|
||||||
min_rating: searchParams.get("min_rating") || undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!validationResult.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Invalid parameters",
|
|
||||||
details: validationResult.error.flatten().fieldErrors,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
q: query,
|
|
||||||
type,
|
|
||||||
page,
|
|
||||||
page_size: pageSize,
|
|
||||||
sort,
|
|
||||||
min_rating,
|
|
||||||
commercial_only,
|
|
||||||
} = validationResult.data;
|
|
||||||
|
|
||||||
if (type === "songs") {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Songs are not available yet",
|
|
||||||
message:
|
|
||||||
"Song search functionality is coming soon. Try searching for sound effects instead.",
|
|
||||||
},
|
|
||||||
{ status: 501 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl = "https://freesound.org/apiv2/search/text/";
|
|
||||||
|
|
||||||
// Use score sorting for search queries, downloads for top sounds
|
|
||||||
const sortParam = query
|
|
||||||
? sort === "score"
|
|
||||||
? "score"
|
|
||||||
: `${sort}_desc`
|
|
||||||
: `${sort}_desc`;
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
query: query || "",
|
|
||||||
token: env.FREESOUND_API_KEY,
|
|
||||||
page: page.toString(),
|
|
||||||
page_size: pageSize.toString(),
|
|
||||||
sort: sortParam,
|
|
||||||
fields:
|
|
||||||
"id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always apply sound effect filters (since we're primarily a sound effects search)
|
|
||||||
if (type === "effects" || !type) {
|
|
||||||
params.append("filter", "duration:[* TO 30.0]");
|
|
||||||
params.append("filter", `avg_rating:[${min_rating} TO *]`);
|
|
||||||
|
|
||||||
// Filter by license if commercial_only is true
|
|
||||||
if (commercial_only) {
|
|
||||||
params.append(
|
|
||||||
"filter",
|
|
||||||
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
params.append(
|
|
||||||
"filter",
|
|
||||||
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}?${params.toString()}`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error("Freesound API error:", response.status, errorText);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Failed to search sounds" },
|
|
||||||
{ status: response.status }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawData = await response.json();
|
|
||||||
|
|
||||||
const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
|
|
||||||
if (!freesoundValidation.success) {
|
|
||||||
console.error(
|
|
||||||
"Invalid Freesound API response:",
|
|
||||||
freesoundValidation.error
|
|
||||||
);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid response from Freesound API" },
|
|
||||||
{ status: 502 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = freesoundValidation.data;
|
|
||||||
|
|
||||||
const transformedResults = data.results.map((result) => ({
|
|
||||||
id: result.id,
|
|
||||||
name: result.name,
|
|
||||||
description: result.description,
|
|
||||||
url: result.url,
|
|
||||||
previewUrl:
|
|
||||||
result.previews?.["preview-hq-mp3"] ||
|
|
||||||
result.previews?.["preview-lq-mp3"],
|
|
||||||
downloadUrl: result.download,
|
|
||||||
duration: result.duration,
|
|
||||||
filesize: result.filesize,
|
|
||||||
type: result.type,
|
|
||||||
channels: result.channels,
|
|
||||||
bitrate: result.bitrate,
|
|
||||||
bitdepth: result.bitdepth,
|
|
||||||
samplerate: result.samplerate,
|
|
||||||
username: result.username,
|
|
||||||
tags: result.tags,
|
|
||||||
license: result.license,
|
|
||||||
created: result.created,
|
|
||||||
downloads: result.num_downloads || 0,
|
|
||||||
rating: result.avg_rating || 0,
|
|
||||||
ratingCount: result.num_ratings || 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const responseData = {
|
|
||||||
count: data.count,
|
|
||||||
next: data.next,
|
|
||||||
previous: data.previous,
|
|
||||||
results: transformedResults,
|
|
||||||
query: query || "",
|
|
||||||
type: type || "effects",
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sort,
|
|
||||||
minRating: min_rating,
|
|
||||||
};
|
|
||||||
|
|
||||||
const responseValidation = apiResponseSchema.safeParse(responseData);
|
|
||||||
if (!responseValidation.success) {
|
|
||||||
console.error(
|
|
||||||
"Invalid API response structure:",
|
|
||||||
responseValidation.error
|
|
||||||
);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal response formatting error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(responseValidation.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error searching sounds:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal server error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { env } from "@/env";
|
|
||||||
import { baseRateLimit } from "@/lib/rate-limit";
|
|
||||||
import { isTranscriptionConfigured } from "@/lib/transcription-utils";
|
|
||||||
|
|
||||||
const transcribeRequestSchema = z.object({
|
|
||||||
filename: z.string().min(1, "Filename is required"),
|
|
||||||
language: z.string().optional().default("auto"),
|
|
||||||
decryptionKey: z.string().min(1, "Decryption key is required").optional(),
|
|
||||||
iv: z.string().min(1, "IV is required").optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalResponseSchema = z.object({
|
|
||||||
text: z.string(),
|
|
||||||
segments: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.number(),
|
|
||||||
seek: z.number(),
|
|
||||||
start: z.number(),
|
|
||||||
end: z.number(),
|
|
||||||
text: z.string(),
|
|
||||||
tokens: z.array(z.number()),
|
|
||||||
temperature: z.number(),
|
|
||||||
avg_logprob: z.number(),
|
|
||||||
compression_ratio: z.number(),
|
|
||||||
no_speech_prob: z.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
language: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiResponseSchema = z.object({
|
|
||||||
text: z.string(),
|
|
||||||
segments: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.number(),
|
|
||||||
seek: z.number(),
|
|
||||||
start: z.number(),
|
|
||||||
end: z.number(),
|
|
||||||
text: z.string(),
|
|
||||||
tokens: z.array(z.number()),
|
|
||||||
temperature: z.number(),
|
|
||||||
avg_logprob: z.number(),
|
|
||||||
compression_ratio: z.number(),
|
|
||||||
no_speech_prob: z.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
language: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Rate limiting
|
|
||||||
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
|
|
||||||
const { success } = await baseRateLimit.limit(ip);
|
|
||||||
const origin = request.headers.get("origin");
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check transcription configuration
|
|
||||||
const transcriptionCheck = isTranscriptionConfigured();
|
|
||||||
if (!transcriptionCheck.configured) {
|
|
||||||
console.error(
|
|
||||||
"Missing environment variables:",
|
|
||||||
JSON.stringify(transcriptionCheck.missingVars)
|
|
||||||
);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Transcription not configured",
|
|
||||||
message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
|
|
||||||
},
|
|
||||||
{ status: 503 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and validate request body
|
|
||||||
const rawBody = await request.json().catch(() => null);
|
|
||||||
if (!rawBody) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid JSON in request body" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationResult = transcribeRequestSchema.safeParse(rawBody);
|
|
||||||
if (!validationResult.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Invalid request parameters",
|
|
||||||
details: validationResult.error.flatten().fieldErrors,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { filename, language, decryptionKey, iv } = validationResult.data;
|
|
||||||
|
|
||||||
// Prepare request body for Modal
|
|
||||||
const modalRequestBody: any = {
|
|
||||||
filename,
|
|
||||||
language,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add encryption parameters if provided (zero-knowledge)
|
|
||||||
if (decryptionKey && iv) {
|
|
||||||
modalRequestBody.decryptionKey = decryptionKey;
|
|
||||||
modalRequestBody.iv = iv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Modal transcription service
|
|
||||||
const response = await fetch(env.MODAL_TRANSCRIPTION_URL, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(modalRequestBody),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error("Modal API error:", response.status, errorText);
|
|
||||||
|
|
||||||
let errorMessage = "Transcription service unavailable";
|
|
||||||
try {
|
|
||||||
const errorData = JSON.parse(errorText);
|
|
||||||
errorMessage = errorData.error || errorMessage;
|
|
||||||
} catch {
|
|
||||||
// Use default message if parsing fails
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: errorMessage,
|
|
||||||
message: "Failed to process transcription request",
|
|
||||||
},
|
|
||||||
{ status: response.status >= 500 ? 502 : response.status }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawResult = await response.json();
|
|
||||||
console.log("Raw Modal response:", JSON.stringify(rawResult, null, 2));
|
|
||||||
|
|
||||||
// Validate Modal response
|
|
||||||
const modalValidation = modalResponseSchema.safeParse(rawResult);
|
|
||||||
if (!modalValidation.success) {
|
|
||||||
console.error("Invalid Modal API response:", modalValidation.error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid response from transcription service" },
|
|
||||||
{ status: 502 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = modalValidation.data;
|
|
||||||
|
|
||||||
// Prepare and validate API response
|
|
||||||
const responseData = {
|
|
||||||
text: result.text,
|
|
||||||
segments: result.segments,
|
|
||||||
language: result.language,
|
|
||||||
};
|
|
||||||
|
|
||||||
const responseValidation = apiResponseSchema.safeParse(responseData);
|
|
||||||
if (!responseValidation.success) {
|
|
||||||
console.error(
|
|
||||||
"Invalid API response structure:",
|
|
||||||
responseValidation.error
|
|
||||||
);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal response formatting error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(responseValidation.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Transcription API error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Internal server error",
|
|
||||||
message: "An unexpected error occurred during transcription",
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { baseRateLimit } from "@/lib/rate-limit";
|
|
||||||
import { db, exportWaitlist, eq } from "@opencut/db";
|
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
import {
|
|
||||||
exportWaitlistSchema,
|
|
||||||
exportWaitlistResponseSchema,
|
|
||||||
} from "@/lib/schemas/waitlist";
|
|
||||||
|
|
||||||
const requestSchema = exportWaitlistSchema;
|
|
||||||
const responseSchema = exportWaitlistResponseSchema;
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
|
|
||||||
const { success } = await baseRateLimit.limit(ip);
|
|
||||||
if (!success) {
|
|
||||||
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json().catch(() => null);
|
|
||||||
if (!body) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Invalid JSON in request body" },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = requestSchema.safeParse(body);
|
|
||||||
if (!parsed.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: "Invalid request parameters",
|
|
||||||
details: parsed.error.flatten().fieldErrors,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email } = parsed.data;
|
|
||||||
|
|
||||||
const existing = await db
|
|
||||||
.select({ id: exportWaitlist.id })
|
|
||||||
.from(exportWaitlist)
|
|
||||||
.where(eq(exportWaitlist.email, email))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existing.length > 0) {
|
|
||||||
const responseData = { success: true, alreadySubscribed: true } as const;
|
|
||||||
const validated = responseSchema.safeParse(responseData);
|
|
||||||
if (!validated.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal response formatting error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(validated.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.insert(exportWaitlist).values({
|
|
||||||
id: randomUUID(),
|
|
||||||
email,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseData = { success: true } as const;
|
|
||||||
const validated = responseSchema.safeParse(responseData);
|
|
||||||
if (!validated.success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal response formatting error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.json(validated.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Waitlist API error:", error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal server error" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import { Header } from "@/components/header";
|
|
||||||
import Prose from "@/components/ui/prose";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog-query";
|
|
||||||
import { Metadata } from "next";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
|
|
||||||
type PageProps = {
|
|
||||||
params: Promise<{ slug: string }>;
|
|
||||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
|
||||||
params,
|
|
||||||
}: PageProps): Promise<Metadata> {
|
|
||||||
const slug = (await params).slug;
|
|
||||||
|
|
||||||
const data = await getSinglePost(slug);
|
|
||||||
|
|
||||||
if (!data || !data.post) return {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: data.post.title,
|
|
||||||
description: data.post.description,
|
|
||||||
twitter: {
|
|
||||||
title: `${data.post.title}`,
|
|
||||||
description: `${data.post.description}`,
|
|
||||||
card: "summary_large_image",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: data.post.coverImage,
|
|
||||||
width: "1200",
|
|
||||||
height: "630",
|
|
||||||
alt: data.post.title,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
openGraph: {
|
|
||||||
type: "article",
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: data.post.coverImage,
|
|
||||||
width: "1200",
|
|
||||||
height: "630",
|
|
||||||
alt: data.post.title,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
title: data.post.title,
|
|
||||||
description: data.post.description,
|
|
||||||
publishedTime: new Date(data.post.publishedAt).toISOString(),
|
|
||||||
authors: [
|
|
||||||
...data.post.authors.map((author: { name: string }) => author.name),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
|
||||||
const data = await getPosts();
|
|
||||||
if (!data || !data.posts.length) return [];
|
|
||||||
|
|
||||||
return data.posts.map((post) => ({
|
|
||||||
slug: post.slug,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function Page({ params }: PageProps) {
|
|
||||||
const slug = (await params).slug;
|
|
||||||
const data = await getSinglePost(slug);
|
|
||||||
if (!data || !data.post) return notFound();
|
|
||||||
|
|
||||||
const html = await processHtmlContent(data.post.content);
|
|
||||||
|
|
||||||
const formattedDate = new Date(data.post.publishedAt).toLocaleDateString(
|
|
||||||
"en-US",
|
|
||||||
{
|
|
||||||
day: "numeric",
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<main className="relative">
|
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
||||||
<div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
|
||||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative container max-w-3xl mx-auto px-4 py-16">
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
{data.post.coverImage && (
|
|
||||||
<div className="relative aspect-video rounded-lg overflow-hidden mb-6">
|
|
||||||
<Image
|
|
||||||
src={data.post.coverImage}
|
|
||||||
alt={data.post.title}
|
|
||||||
loading="eager"
|
|
||||||
fill
|
|
||||||
className="object-cover rounded-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center justify-center mb-6">
|
|
||||||
<time dateTime={data.post.publishedAt.toString()}>
|
|
||||||
{formattedDate}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="text-5xl md:text-4xl font-bold tracking-tight mb-6">
|
|
||||||
{data.post.title}
|
|
||||||
</h1>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
{data.post.authors[0] && (
|
|
||||||
<>
|
|
||||||
<Image
|
|
||||||
src={data.post.authors[0].image}
|
|
||||||
alt={data.post.authors[0].name}
|
|
||||||
width={36}
|
|
||||||
height={36}
|
|
||||||
loading="eager"
|
|
||||||
className="aspect-square shrink-0 size-8 rounded-full"
|
|
||||||
/>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
{data.post.authors[0].name}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<section className="mt-14">
|
|
||||||
<Prose html={html} />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { Metadata } from "next";
|
|
||||||
import { Header } from "@/components/header";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { getPosts } from "@/lib/blog-query";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Blog - OpenCut",
|
|
||||||
description:
|
|
||||||
"Read the latest news and updates about OpenCut, the free and open-source video editor.",
|
|
||||||
openGraph: {
|
|
||||||
title: "Blog - OpenCut",
|
|
||||||
description:
|
|
||||||
"Read the latest news and updates about OpenCut, the free and open-source video editor.",
|
|
||||||
type: "website",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function BlogPage() {
|
|
||||||
const data = await getPosts();
|
|
||||||
if (!data || !data.posts) return <div>No posts yet</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background">
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<main className="relative">
|
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
||||||
<div className="absolute -top-40 -right-40 w-96 h-96 bg-linear-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
|
||||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-linear-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative container max-w-3xl mx-auto px-4 py-16">
|
|
||||||
<div className="text-center mb-20">
|
|
||||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
|
|
||||||
Blog
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
|
|
||||||
Read the latest news and updates about OpenCut, the free and
|
|
||||||
open-source video editor.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
{data.posts.map((post) => (
|
|
||||||
<Link key={post.id} href={`/blog/${post.slug}`}>
|
|
||||||
<Card className="h-full hover:shadow-lg transition-shadow overflow-hidden">
|
|
||||||
{post.coverImage && (
|
|
||||||
<div className="relative aspect-video">
|
|
||||||
<Image
|
|
||||||
src={post.coverImage}
|
|
||||||
alt={post.title}
|
|
||||||
fill
|
|
||||||
className="object-cover rounded-xl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CardContent className="p-6">
|
|
||||||
{post.authors && post.authors.length > 0 && (
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
{post.authors.map((author, index) => (
|
|
||||||
<div
|
|
||||||
key={author.id}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Avatar className="w-6 h-6">
|
|
||||||
<AvatarImage
|
|
||||||
src={author.image}
|
|
||||||
alt={author.name}
|
|
||||||
/>
|
|
||||||
<AvatarFallback className="text-xs">
|
|
||||||
{author.name.charAt(0).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{author.name}
|
|
||||||
</span>
|
|
||||||
{index < post.authors.length - 1 && (
|
|
||||||
<span className="text-muted-foreground">•</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<h2 className="text-xl font-semibold mb-2">{post.title}</h2>
|
|
||||||
<p className="text-muted-foreground">{post.description}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||