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 | ||
|
|
88304c8a90 | ||
|
|
60debe2ce5 | ||
|
|
167e2b83dc | ||
|
|
c5c32a1e34 | ||
|
|
f7ef8d7f38 | ||
|
|
42cf851d63 | ||
|
|
c5480d307c | ||
|
|
3d67b065f5 | ||
|
|
9c00da37b9 | ||
|
|
d8f446580c | ||
|
|
b84e699e9c | ||
|
|
af1964cf11 | ||
|
|
e2bd8d1e09 | ||
|
|
971f9d35d7 | ||
|
|
dd02e31380 | ||
|
|
350b55081c | ||
|
|
2b700b7474 | ||
|
|
e43d79e4c5 | ||
|
|
45d0caf979 | ||
|
|
6e32d4aed5 | ||
|
|
8eabbeae7c | ||
|
|
2c5d5dc234 | ||
|
|
46a74b30ba | ||
|
|
73a156c002 | ||
|
|
29fdd8568d | ||
|
|
3db7666467 | ||
|
|
ae50aee4b6 | ||
|
|
e7d75ce1f8 | ||
|
|
d747fb458d | ||
|
|
e222f15d1d | ||
|
|
21dac9f09e | ||
|
|
37d4014808 | ||
|
|
5abddb0a22 | ||
|
|
598afab08b | ||
|
|
2afd4ec6fb | ||
|
|
e98e40028e | ||
|
|
454a308644 | ||
|
|
886cf81bd0 | ||
|
|
2d834a49d7 | ||
|
|
a7d90f7eac | ||
|
|
70a9bfebb1 | ||
|
|
d4e08bb7e3 | ||
|
|
06e3d0ca2a | ||
|
|
f9170dcf09 | ||
|
|
ebe1b38bdf | ||
|
|
88c4d160e0 | ||
|
|
163b3599dc | ||
|
|
adc8cb37fc | ||
|
|
b459b46802 | ||
|
|
2f7cd11ca1 | ||
|
|
9497bef10a | ||
|
|
74d45a7913 | ||
|
|
7434f90949 | ||
|
|
03e68e77a9 | ||
|
|
a4f2273e25 | ||
|
|
7756ed0d80 | ||
|
|
721a28e9a7 | ||
|
|
fedde33cba | ||
|
|
0e3c4ef13b | ||
|
|
ffc1d7526e | ||
|
|
f750c45765 | ||
|
|
792fb6cdc0 | ||
|
|
756a7c7caa | ||
|
|
17c2ba2fc8 | ||
|
|
13dff382c4 | ||
|
|
f3ddf26334 | ||
|
|
0f1ed40aef | ||
|
|
bb461fa9fb | ||
|
|
a90a0f9740 | ||
|
|
11d2a66c95 | ||
|
|
77d8c6c774 | ||
|
|
c0709293da | ||
|
|
9da0dfc107 | ||
|
|
a0f8e2c1a7 | ||
|
|
96f3373fad | ||
|
|
354b6caedd | ||
|
|
e96682f89e | ||
|
|
1b1486ad94 | ||
|
|
f4e127f2d4 | ||
|
|
8c77095268 | ||
|
|
4621caee19 | ||
|
|
7e4cd76762 | ||
|
|
033a9aa1bc | ||
|
|
aa5cd1edca | ||
|
|
32d263fccc | ||
|
|
7f91c9ca00 | ||
|
|
5e8a489a14 | ||
|
|
c3f3345d7b | ||
|
|
defff2fc46 | ||
|
|
0274fb6626 | ||
|
|
0ae09c9df5 | ||
|
|
f7fddff1fc | ||
|
|
71b6718337 | ||
|
|
c72badb7e9 | ||
|
|
f9aab49080 | ||
|
|
a97eb26fa4 | ||
|
|
ae987e1d66 | ||
|
|
8d3ccf0d56 | ||
|
|
3807e146e4 | ||
|
|
a1e48613b4 | ||
|
|
665bb9fbd1 | ||
|
|
5b059ce6de | ||
|
|
38f8276788 | ||
|
|
fa698c276c | ||
|
|
e908bad167 | ||
|
|
8c205273b7 | ||
|
|
88cb750a3b | ||
|
|
54e990af26 | ||
|
|
e71788014d | ||
|
|
effb6a8641 | ||
|
|
ce58b5199a | ||
|
|
2c90ddd5d5 | ||
|
|
e5a0deb777 | ||
|
|
6aaa9a1f58 | ||
|
|
e724a08684 | ||
|
|
43c8b59c80 | ||
|
|
f8724851fd | ||
|
|
4f6d014286 | ||
|
|
b83e6bedb7 | ||
|
|
9c6556ea05 | ||
|
|
2a7cc13d3e | ||
|
|
eafea56e99 | ||
|
|
539cd45f64 | ||
|
|
efbbc48bc5 | ||
|
|
949228f0bb | ||
|
|
e454612000 | ||
|
|
dee434cd33 | ||
|
|
974e3636fe | ||
|
|
65b04f5759 | ||
|
|
8bbb41a3cb | ||
|
|
1bed89d55f | ||
|
|
8f6c487c26 | ||
|
|
b0329f1372 | ||
|
|
f0dd30ebfc | ||
|
|
1c4c690048 | ||
|
|
f7cd3ebac9 | ||
|
|
9ec5a02252 | ||
|
|
2fcc5e0643 | ||
|
|
0386624ff5 | ||
|
|
efa5be2ab8 | ||
|
|
b0cfedbc5a | ||
|
|
06d99cb6be | ||
|
|
81de82ffaa | ||
|
|
21b248718b | ||
|
|
bb36b2e439 | ||
|
|
3a6bc6ebf7 | ||
|
|
d022a78ae2 | ||
|
|
4cd8100b6b | ||
|
|
2fddbf071d | ||
|
|
d3ace6a946 | ||
|
|
acc2089d0d | ||
|
|
35ade4b25f | ||
|
|
48b694bbd8 | ||
|
|
b52a9ca3e2 | ||
|
|
c0651fec19 | ||
|
|
f50e250ec6 | ||
|
|
02a50d12de | ||
|
|
5e2b07e518 | ||
|
|
c8980de41a | ||
|
|
ae54aff1a9 | ||
|
|
2c0423667c | ||
|
|
e5a5b76ece | ||
|
|
14393c559d | ||
|
|
7891ed2cc1 | ||
|
|
dd09bf2949 | ||
|
|
cd593d1b98 | ||
|
|
68da487f30 | ||
|
|
09cb8c8d98 | ||
|
|
6412158003 | ||
|
|
b00d6d5fb6 | ||
|
|
f5d5a387d3 | ||
|
|
60d4b0c3d6 | ||
|
|
c5baa3fed1 | ||
|
|
5475cea0de | ||
|
|
c60ec04f8d | ||
|
|
732e9f54a8 | ||
|
|
1603dd0e58 | ||
|
|
3c64f9cfa2 | ||
|
|
6899450df9 | ||
|
|
10afbda9dd | ||
|
|
2bde71d4bc | ||
|
|
83b568402a | ||
|
|
b2cd730fb7 | ||
|
|
5f4f3cc6c4 | ||
|
|
f255ccb818 | ||
|
|
a1d2fbd96b | ||
|
|
b229d94a8d | ||
|
|
cd5642161b | ||
|
|
48282555e0 | ||
|
|
e3ecf4855a | ||
|
|
dbc1d2e1a3 | ||
|
|
df1e80b528 | ||
|
|
e6e08057f5 | ||
|
|
f15508df62 | ||
|
|
281ec63d7e | ||
|
|
1c20027d33 | ||
|
|
b9e5bdff95 | ||
|
|
17ab403489 | ||
|
|
91c95b12d1 | ||
|
|
397421159b | ||
|
|
004fa0c12b | ||
|
|
da0e768b32 | ||
|
|
deda0e366f | ||
|
|
118d9a9890 | ||
|
|
95c53e441b | ||
|
|
726d9cb90b | ||
|
|
fbff145036 | ||
|
|
073cb817d3 | ||
|
|
2cf02dd90a | ||
|
|
2e0c9f4b23 | ||
|
|
f6f26fc635 | ||
|
|
ef5d8c25eb | ||
|
|
df3b092010 | ||
|
|
609e115977 | ||
|
|
6ab9f25658 | ||
|
|
d154937ac2 | ||
|
|
906f95399d | ||
|
|
14475dc9a6 | ||
|
|
f149ab3b6c | ||
|
|
6dfc80cd70 | ||
|
|
043bf7d70a | ||
|
|
317ee9141e | ||
|
|
f312d758da | ||
|
|
2a1d9f3c34 | ||
|
|
309a2ecb61 | ||
|
|
4bcefb1a6f | ||
|
|
b0bcca18f8 | ||
|
|
76ee723c37 | ||
|
|
ae94056952 | ||
|
|
7d0113d366 | ||
|
|
a4f1a02cea | ||
|
|
c51e82db9b | ||
|
|
00bcce8eca | ||
|
|
70fe5cacd1 | ||
|
|
87a8ac90e5 | ||
|
|
4f60c0d354 | ||
|
|
c34f3994d9 | ||
|
|
8c72b75bb6 | ||
|
|
a369b606a5 | ||
|
|
4e0d2c20f6 | ||
|
|
642f51dac5 | ||
|
|
812bab4edf | ||
|
|
b288cc0eee | ||
|
|
c73df818d2 | ||
|
|
411fcaccf1 | ||
|
|
5946b84b4b | ||
|
|
d667c1095c | ||
|
|
9a18a7a4e9 | ||
|
|
e3504de1f1 | ||
|
|
7e4a4e2095 | ||
|
|
8dcbbfa252 | ||
|
|
8e737d87e0 | ||
|
|
1ccacf6bb5 | ||
|
|
65c3f9f7f7 | ||
|
|
262a7d53d6 | ||
|
|
5931ddb77a | ||
|
|
7b840e9b4e | ||
|
|
cf039e48a9 | ||
|
|
33acf2497d | ||
|
|
275a23f583 | ||
|
|
5a7745f806 | ||
|
|
e8dc0f62da | ||
|
|
c1b6400612 | ||
|
|
4b882d005f | ||
|
|
0b7a943de3 | ||
|
|
b8671eb761 | ||
|
|
1504b4f60c | ||
|
|
7bd952b080 | ||
|
|
b208f355e2 | ||
|
|
9e711c1e09 | ||
|
|
8be8f0a5a5 | ||
|
|
81ad5c1782 | ||
|
|
17d1336c80 | ||
|
|
45096b7b65 | ||
|
|
41a78b4e4c | ||
|
|
f39f46788f | ||
|
|
184dcbabef | ||
|
|
57243bc4b4 | ||
|
|
8208d931d0 | ||
|
|
b87f470dc4 | ||
|
|
e0fac61a91 | ||
|
|
92079777b9 | ||
|
|
f6073c47c6 | ||
|
|
f98651cd33 | ||
|
|
771591bdfa | ||
|
|
f3d5eb9d08 | ||
|
|
a434459a9c | ||
|
|
901ddb509d | ||
|
|
4fb6018cf4 | ||
|
|
ec87526842 | ||
|
|
cf6ef8f1c4 | ||
|
|
cabf860330 | ||
|
|
68492df302 | ||
|
|
4e808b6c46 | ||
|
|
4350f9898e | ||
|
|
6de16395b7 | ||
|
|
40aac17cfb | ||
|
|
87a29963c3 | ||
|
|
747630ef3f | ||
|
|
a7c0b92051 | ||
|
|
fe0175c15e | ||
|
|
2791992b6b | ||
|
|
097c574d9c | ||
|
|
e7b89a4cc0 | ||
|
|
fb5dca9f9d | ||
|
|
a5f7b477e6 | ||
|
|
853509dd2c | ||
|
|
205ad520f0 | ||
|
|
26f5966ebd | ||
|
|
b6a8ca79f6 | ||
|
|
c783062121 | ||
|
|
301edd0749 | ||
|
|
f2a403b678 | ||
|
|
0ba2893610 | ||
|
|
550f5363fd | ||
|
|
2bc7ae0ea3 | ||
|
|
2bc3df5383 | ||
|
|
c06ff9249d | ||
|
|
cf8c401a2a | ||
|
|
58b42b8dcd | ||
|
|
169e3d698b | ||
|
|
ab82db6346 | ||
|
|
6c41348295 | ||
|
|
53d00b7274 | ||
|
|
d364bffc1f | ||
|
|
d27f05be92 | ||
|
|
2517f77d42 | ||
|
|
6ba75a28f5 | ||
|
|
18296402cb | ||
|
|
3c76a3f241 | ||
|
|
768a1dbfbc | ||
|
|
d6724e2552 | ||
|
|
b555d65ea6 | ||
|
|
35abbefd2e | ||
|
|
5a3fa8fbe1 | ||
|
|
fdb13c2130 | ||
|
|
fbb8446283 | ||
|
|
2f5dd8e12c | ||
|
|
3cd968d423 | ||
|
|
3f3fad1a23 | ||
|
|
6f8c864016 | ||
|
|
5915989669 | ||
|
|
a2c21dc3c4 | ||
|
|
b03287df8d | ||
|
|
061624ab58 |
@@ -1 +0,0 @@
|
||||
!apps/web/.env.example
|
||||
@@ -1,90 +1,89 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[INSERT CONTACT METHOD].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at our discord server.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
|
||||
@@ -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,70 +1,70 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
body:
|
||||
- type: input
|
||||
id: Platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Please enter the platform on which you encountered the bug.
|
||||
placeholder: e.g. Windows 11, Ubuntu 14.04
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: Browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Please enter the browser on which you encountered the bug.
|
||||
placeholder: e.g. Chrome 137, Firefox 137, Safari 17
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: recurrence-probability
|
||||
attributes:
|
||||
label: Recurrence Probability
|
||||
description: How often does this bug occur?
|
||||
options:
|
||||
- Always
|
||||
- Usually
|
||||
- Sometimes
|
||||
- Seldom
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
body:
|
||||
- type: input
|
||||
id: Platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Please enter the platform on which you encountered the bug.
|
||||
placeholder: e.g. Windows 11, Ubuntu 14.04
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: Browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Please enter the browser on which you encountered the bug.
|
||||
placeholder: e.g. Chrome 137, Firefox 137, Safari 17
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: recurrence-probability
|
||||
attributes:
|
||||
label: Recurrence Probability
|
||||
description: How often does this bug occur?
|
||||
options:
|
||||
- Always
|
||||
- Usually
|
||||
- Sometimes
|
||||
- Seldom
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for OpenCut
|
||||
title: '[FEATURE] '
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure that no duplicated issues has already been delivered.
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
placeholder: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Solution
|
||||
placeholder: Describe the solution you'd like.
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: Alternative
|
||||
placeholder: Describe alternatives you've considered.
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
name: Feature request
|
||||
description: Suggest an idea for OpenCut
|
||||
title: "[FEATURE] "
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure that no duplicated issues has already been delivered.
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
placeholder: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Solution
|
||||
placeholder: Describe the solution you'd like.
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative
|
||||
attributes:
|
||||
label: Alternative
|
||||
placeholder: Describe alternatives you've considered.
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take security vulnerabilities seriously. If you discover a security vulnerability within OpenCut, please send an email to security@opencut.app. All security vulnerabilities will be promptly addressed.
|
||||
|
||||
Please do not report security vulnerabilities through public GitHub issues.
|
||||
|
||||
### What to include in your report
|
||||
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Any suggested fixes
|
||||
|
||||
### Response timeline
|
||||
|
||||
- We will acknowledge receipt within 48 hours
|
||||
- We will provide a detailed response within 5 business days
|
||||
- We will keep you updated on our progress
|
||||
|
||||
Thank you for helping keep OpenCut secure!
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.x.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take security vulnerabilities seriously. If you discover a security vulnerability within OpenCut, please send an email to security@opencut.app. All security vulnerabilities will be promptly addressed.
|
||||
|
||||
Please do not report security vulnerabilities through public GitHub issues.
|
||||
|
||||
### What to include in your report
|
||||
|
||||
- Description of the vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Any suggested fixes
|
||||
|
||||
### Response timeline
|
||||
|
||||
- We will acknowledge receipt within 48 hours
|
||||
- We will provide a detailed response within 5 business days
|
||||
- We will keep you updated on our progress
|
||||
|
||||
Thank you for helping keep OpenCut secure!
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
# Getting Help
|
||||
|
||||
Thanks for using OpenCut! If you need help, here are your options:
|
||||
|
||||
## Documentation
|
||||
- Check our [README](../README.md) for basic setup instructions
|
||||
- Review the [Contributing Guidelines](CONTRIBUTING.md) for development setup
|
||||
|
||||
## Issues
|
||||
- **Bug reports**: Use the bug report template
|
||||
- **Feature requests**: Use the feature request template
|
||||
- **Questions**: Use GitHub Discussions for general questions
|
||||
|
||||
## Community
|
||||
- Join our discussions on GitHub
|
||||
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
|
||||
## Response Times
|
||||
- Issues are typically triaged within 2-3 business days
|
||||
- Feature requests may take longer to evaluate
|
||||
- Security issues are handled with priority
|
||||
|
||||
We appreciate your patience and contributions to making OpenCut better!
|
||||
# Getting Help
|
||||
|
||||
Thanks for using OpenCut! If you need help, here are your options:
|
||||
|
||||
## Documentation
|
||||
|
||||
- Check our [README](../README.md) for basic setup instructions
|
||||
- Review the [Contributing Guidelines](CONTRIBUTING.md) for development setup
|
||||
|
||||
## Issues
|
||||
|
||||
- **Bug reports**: Use the bug report template
|
||||
- **Feature requests**: Use the feature request template
|
||||
- **Questions**: Use GitHub Discussions for general questions
|
||||
|
||||
## Community
|
||||
|
||||
- Join our discussions on GitHub
|
||||
- Follow the [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
|
||||
## Response Times
|
||||
|
||||
- Issues are typically triaged within 2-3 business days
|
||||
- Feature requests may take longer to evaluate
|
||||
- Security issues are handled with priority
|
||||
|
||||
We appreciate your patience and contributions to making OpenCut better!
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
---
|
||||
applyTo: "**/*.{ts,tsx,js,jsx}"
|
||||
---
|
||||
|
||||
# Project Context
|
||||
|
||||
Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter.
|
||||
|
||||
## Key Principles
|
||||
|
||||
- Zero configuration required
|
||||
- Subsecond performance
|
||||
- Maximum type safety
|
||||
- AI-friendly code generation
|
||||
|
||||
## Before Writing Code
|
||||
|
||||
1. Analyze existing patterns in the codebase
|
||||
2. Consider edge cases and error scenarios
|
||||
3. Follow the rules below strictly
|
||||
4. Validate accessibility requirements
|
||||
|
||||
## Rules
|
||||
|
||||
### Accessibility (a11y)
|
||||
|
||||
- Don't use `accessKey` attribute on any HTML element.
|
||||
- Don't set `aria-hidden="true"` on focusable elements.
|
||||
- Don't add ARIA roles, states, and properties to elements that don't support them.
|
||||
- Don't use distracting elements like `<marquee>` or `<blink>`.
|
||||
- Only use the `scope` prop on `<th>` elements.
|
||||
- Don't assign non-interactive ARIA roles to interactive HTML elements.
|
||||
- Make sure label elements have text content and are associated with an input.
|
||||
- Don't assign interactive ARIA roles to non-interactive HTML elements.
|
||||
- Don't assign `tabIndex` to non-interactive HTML elements.
|
||||
- Don't use positive integers for `tabIndex` property.
|
||||
- Don't include "image", "picture", or "photo" in img alt prop.
|
||||
- Don't use explicit role property that's the same as the implicit/default role.
|
||||
- Make static elements with click handlers use a valid role attribute.
|
||||
- Always include a `title` element for SVG elements.
|
||||
- Give all elements requiring alt text meaningful information for screen readers.
|
||||
- Make sure anchors have content that's accessible to screen readers.
|
||||
- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`.
|
||||
- Include all required ARIA attributes for elements with ARIA roles.
|
||||
- Make sure ARIA properties are valid for the element's supported roles.
|
||||
- Always include a `type` attribute for button elements.
|
||||
- Make elements with interactive roles and handlers focusable.
|
||||
- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`).
|
||||
- Always include a `lang` attribute on the html element.
|
||||
- Always include a `title` attribute for iframe elements.
|
||||
- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`.
|
||||
- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`.
|
||||
- Include caption tracks for audio and video elements.
|
||||
- Use semantic elements instead of role attributes in JSX.
|
||||
- Make sure all anchors are valid and navigable.
|
||||
- Ensure all ARIA properties (`aria-*`) are valid.
|
||||
- Use valid, non-abstract ARIA roles for elements with ARIA roles.
|
||||
- Use valid ARIA state and property values.
|
||||
- Use valid values for the `autocomplete` attribute on input elements.
|
||||
- Use correct ISO language/country codes for the `lang` attribute.
|
||||
|
||||
### Code Complexity and Quality
|
||||
|
||||
- Don't use consecutive spaces in regular expression literals.
|
||||
- Don't use the `arguments` object.
|
||||
- Don't use primitive type aliases or misleading types.
|
||||
- Don't use the comma operator.
|
||||
- Don't use empty type parameters in type aliases and interfaces.
|
||||
- Don't write functions that exceed a given Cognitive Complexity score.
|
||||
- Don't nest describe() blocks too deeply in test files.
|
||||
- Don't use unnecessary boolean casts.
|
||||
- Don't use unnecessary callbacks with flatMap.
|
||||
- Use for...of statements instead of Array.forEach.
|
||||
- Don't create classes that only have static members (like a static namespace).
|
||||
- Don't use this and super in static contexts.
|
||||
- Don't use unnecessary catch clauses.
|
||||
- Don't use unnecessary constructors.
|
||||
- Don't use unnecessary continue statements.
|
||||
- Don't export empty modules that don't change anything.
|
||||
- Don't use unnecessary escape sequences in regular expression literals.
|
||||
- Don't use unnecessary fragments.
|
||||
- Don't use unnecessary labels.
|
||||
- Don't use unnecessary nested block statements.
|
||||
- Don't rename imports, exports, and destructured assignments to the same name.
|
||||
- Don't use unnecessary string or template literal concatenation.
|
||||
- Don't use String.raw in template literals when there are no escape sequences.
|
||||
- Don't use useless case statements in switch statements.
|
||||
- Don't use ternary operators when simpler alternatives exist.
|
||||
- Don't use useless `this` aliasing.
|
||||
- Don't use any or unknown as type constraints.
|
||||
- Don't initialize variables to undefined.
|
||||
- Don't use the void operators (they're not familiar).
|
||||
- Use arrow functions instead of function expressions.
|
||||
- Use Date.now() to get milliseconds since the Unix Epoch.
|
||||
- Use .flatMap() instead of map().flat() when possible.
|
||||
- Use literal property access instead of computed property access.
|
||||
- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
|
||||
- Use concise optional chaining instead of chained logical expressions.
|
||||
- Use regular expression literals instead of the RegExp constructor when possible.
|
||||
- Don't use number literal object member names that aren't base 10 or use underscore separators.
|
||||
- Remove redundant terms from logical expressions.
|
||||
- Use while loops instead of for loops when you don't need initializer and update expressions.
|
||||
- Don't pass children as props.
|
||||
- Don't reassign const variables.
|
||||
- Don't use constant expressions in conditions.
|
||||
- Don't use `Math.min` and `Math.max` to clamp values when the result is constant.
|
||||
- Don't return a value from a constructor.
|
||||
- Don't use empty character classes in regular expression literals.
|
||||
- Don't use empty destructuring patterns.
|
||||
- Don't call global object properties as functions.
|
||||
- Don't declare functions and vars that are accessible outside their block.
|
||||
- Make sure builtins are correctly instantiated.
|
||||
- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors.
|
||||
- Don't use variables and function parameters before they're declared.
|
||||
- Don't use 8 and 9 escape sequences in string literals.
|
||||
- Don't use literal numbers that lose precision.
|
||||
|
||||
### React and JSX Best Practices
|
||||
|
||||
- Don't use the return value of React.render.
|
||||
- Make sure all dependencies are correctly specified in React hooks.
|
||||
- Make sure all React hooks are called from the top level of component functions.
|
||||
- Don't forget key props in iterators and collection literals.
|
||||
- Don't destructure props inside JSX components in Solid projects.
|
||||
- Don't define React components inside other components.
|
||||
- Don't use event handlers on non-interactive elements.
|
||||
- Don't assign to React component props.
|
||||
- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element.
|
||||
- Don't use dangerous JSX props.
|
||||
- Don't use Array index in keys.
|
||||
- Don't insert comments as text nodes.
|
||||
- Don't assign JSX properties multiple times.
|
||||
- Don't add extra closing tags for components without children.
|
||||
- Use `<>...</>` instead of `<Fragment>...</Fragment>`.
|
||||
- Watch out for possible "wrong" semicolons inside JSX elements.
|
||||
|
||||
### Correctness and Safety
|
||||
|
||||
- Don't assign a value to itself.
|
||||
- Don't return a value from a setter.
|
||||
- Don't compare expressions that modify string case with non-compliant values.
|
||||
- Don't use lexical declarations in switch clauses.
|
||||
- Don't use variables that haven't been declared in the document.
|
||||
- Don't write unreachable code.
|
||||
- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass.
|
||||
- Don't use control flow statements in finally blocks.
|
||||
- Don't use optional chaining where undefined values aren't allowed.
|
||||
- Don't have unused function parameters.
|
||||
- Don't have unused imports.
|
||||
- Don't have unused labels.
|
||||
- Don't have unused private class members.
|
||||
- Don't have unused variables.
|
||||
- Make sure void (self-closing) elements don't have children.
|
||||
- Don't return a value from a function with the return type 'void'
|
||||
- Use isNaN() when checking for NaN.
|
||||
- Make sure "for" loop update clauses move the counter in the right direction.
|
||||
- Make sure typeof expressions are compared to valid values.
|
||||
- Make sure generator functions contain yield.
|
||||
- Don't use await inside loops.
|
||||
- Don't use bitwise operators.
|
||||
- Don't use expressions where the operation doesn't change the value.
|
||||
- Make sure Promise-like statements are handled appropriately.
|
||||
- Don't use **dirname and **filename in the global scope.
|
||||
- Prevent import cycles.
|
||||
- Don't use configured elements.
|
||||
- Don't hardcode sensitive data like API keys and tokens.
|
||||
- Don't let variable declarations shadow variables from outer scopes.
|
||||
- Don't use the TypeScript directive @ts-ignore.
|
||||
- Prevent duplicate polyfills from Polyfill.io.
|
||||
- Don't use useless backreferences in regular expressions that always match empty strings.
|
||||
- Don't use unnecessary escapes in string literals.
|
||||
- Don't use useless undefined.
|
||||
- Make sure getters and setters for the same property are next to each other in class and object definitions.
|
||||
- Make sure object literals are declared consistently (defaults to explicit definitions).
|
||||
- Use static Response methods instead of new Response() constructor when possible.
|
||||
- Make sure switch-case statements are exhaustive.
|
||||
- Make sure the `preconnect` attribute is used when using Google Fonts.
|
||||
- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item.
|
||||
- Make sure iterable callbacks return consistent values.
|
||||
- Use `with { type: "json" }` for JSON module imports.
|
||||
- Use numeric separators in numeric literals.
|
||||
- Use object spread instead of `Object.assign()` when constructing new objects.
|
||||
- Always use the radix argument when using `parseInt()`.
|
||||
- Make sure JSDoc comment lines start with a single asterisk, except for the first one.
|
||||
- Include a description parameter for `Symbol()`.
|
||||
- Don't use spread (`...`) syntax on accumulators.
|
||||
- Don't use the `delete` operator.
|
||||
- Don't access namespace imports dynamically.
|
||||
- Don't use namespace imports.
|
||||
- Declare regex literals at the top level.
|
||||
- Don't use `target="_blank"` without `rel="noopener"`.
|
||||
|
||||
### TypeScript Best Practices
|
||||
|
||||
- Don't use TypeScript enums.
|
||||
- Don't export imported variables.
|
||||
- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
|
||||
- Don't use TypeScript namespaces.
|
||||
- Don't use non-null assertions with the `!` postfix operator.
|
||||
- Don't use parameter properties in class constructors.
|
||||
- Don't use user-defined types.
|
||||
- Use `as const` instead of literal types and type annotations.
|
||||
- Use either `T[]` or `Array<T>` consistently.
|
||||
- Initialize each enum member value explicitly.
|
||||
- Use `export type` for types.
|
||||
- Use `import type` for types.
|
||||
- Make sure all enum members are literal values.
|
||||
- Don't use TypeScript const enum.
|
||||
- Don't declare empty interfaces.
|
||||
- Don't let variables evolve into any type through reassignments.
|
||||
- Don't use the any type.
|
||||
- Don't misuse the non-null assertion operator (!) in TypeScript files.
|
||||
- Don't use implicit any type on variable declarations.
|
||||
- Don't merge interfaces and classes unsafely.
|
||||
- Don't use overload signatures that aren't next to each other.
|
||||
- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
|
||||
|
||||
### Style and Consistency
|
||||
|
||||
- Don't use global `eval()`.
|
||||
- Don't use callbacks in asynchronous tests and hooks.
|
||||
- Don't use negation in `if` statements that have `else` clauses.
|
||||
- Don't use nested ternary expressions.
|
||||
- Don't reassign function parameters.
|
||||
- This rule lets you specify global variable names you don't want to use in your application.
|
||||
- Don't use specified modules when loaded by import or require.
|
||||
- Don't use constants whose value is the upper-case version of their name.
|
||||
- Use `String.slice()` instead of `String.substr()` and `String.substring()`.
|
||||
- Don't use template literals if you don't need interpolation or special-character handling.
|
||||
- Don't use `else` blocks when the `if` block breaks early.
|
||||
- Don't use yoda expressions.
|
||||
- Don't use Array constructors.
|
||||
- Use `at()` instead of integer index access.
|
||||
- Follow curly brace conventions.
|
||||
- Use `else if` instead of nested `if` statements in `else` clauses.
|
||||
- Use single `if` statements instead of nested `if` clauses.
|
||||
- Use `new` for all builtins except `String`, `Number`, and `Boolean`.
|
||||
- Use consistent accessibility modifiers on class properties and methods.
|
||||
- Use `const` declarations for variables that are only assigned once.
|
||||
- Put default function parameters and optional function parameters last.
|
||||
- Include a `default` clause in switch statements.
|
||||
- Use the `**` operator instead of `Math.pow`.
|
||||
- Use `for-of` loops when you need the index to extract an item from the iterated array.
|
||||
- Use `node:assert/strict` over `node:assert`.
|
||||
- Use the `node:` protocol for Node.js builtin modules.
|
||||
- Use Number properties instead of global ones.
|
||||
- Use assignment operator shorthand where possible.
|
||||
- Use function types instead of object types with call signatures.
|
||||
- Use template literals over string concatenation.
|
||||
- Use `new` when throwing an error.
|
||||
- Don't throw non-Error values.
|
||||
- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`.
|
||||
- Use standard constants instead of approximated literals.
|
||||
- Don't assign values in expressions.
|
||||
- Don't use async functions as Promise executors.
|
||||
- Don't reassign exceptions in catch clauses.
|
||||
- Don't reassign class members.
|
||||
- Don't compare against -0.
|
||||
- Don't use labeled statements that aren't loops.
|
||||
- Don't use void type outside of generic or return types.
|
||||
- Don't use console.
|
||||
- Don't use control characters and escape sequences that match control characters in regular expression literals.
|
||||
- Don't use debugger.
|
||||
- Don't assign directly to document.cookie.
|
||||
- Use `===` and `!==`.
|
||||
- Don't use duplicate case labels.
|
||||
- Don't use duplicate class members.
|
||||
- Don't use duplicate conditions in if-else-if chains.
|
||||
- Don't use two keys with the same name inside objects.
|
||||
- Don't use duplicate function parameter names.
|
||||
- Don't have duplicate hooks in describe blocks.
|
||||
- Don't use empty block statements and static blocks.
|
||||
- Don't let switch clauses fall through.
|
||||
- Don't reassign function declarations.
|
||||
- Don't allow assignments to native objects and read-only global variables.
|
||||
- Use Number.isFinite instead of global isFinite.
|
||||
- Use Number.isNaN instead of global isNaN.
|
||||
- Don't assign to imported bindings.
|
||||
- Don't use irregular whitespace characters.
|
||||
- Don't use labels that share a name with a variable.
|
||||
- Don't use characters made with multiple code points in character class syntax.
|
||||
- Make sure to use new and constructor properly.
|
||||
- Don't use shorthand assign when the variable appears on both sides.
|
||||
- Don't use octal escape sequences in string literals.
|
||||
- Don't use Object.prototype builtins directly.
|
||||
- Don't redeclare variables, functions, classes, and types in the same scope.
|
||||
- Don't have redundant "use strict".
|
||||
- Don't compare things where both sides are exactly the same.
|
||||
- Don't let identifiers shadow restricted names.
|
||||
- Don't use sparse arrays (arrays with holes).
|
||||
- Don't use template literal placeholder syntax in regular strings.
|
||||
- Don't use the then property.
|
||||
- Don't use unsafe negation.
|
||||
- Don't use var.
|
||||
- Don't use with statements in non-strict contexts.
|
||||
- Make sure async functions actually use await.
|
||||
- Make sure default clauses in switch statements come last.
|
||||
- Make sure to pass a message value when creating a built-in error.
|
||||
- Make sure get methods always return a value.
|
||||
- Use a recommended display strategy with Google Fonts.
|
||||
- Make sure for-in loops include an if statement.
|
||||
- Use Array.isArray() instead of instanceof Array.
|
||||
- Make sure to use the digits argument with Number#toFixed().
|
||||
- Make sure to use the "use strict" directive in script files.
|
||||
|
||||
### Next.js Specific Rules
|
||||
|
||||
- Don't use `<img>` elements in Next.js projects.
|
||||
- Don't use `<head>` elements in Next.js projects.
|
||||
- Don't import next/document outside of pages/\_document.jsx in Next.js projects.
|
||||
- Don't use the next/head module in pages/\_document.js on Next.js projects.
|
||||
|
||||
### Testing Best Practices
|
||||
|
||||
- Don't use export or module.exports in test files.
|
||||
- Don't use focused tests.
|
||||
- Make sure the assertion function, like expect, is placed inside an it() function call.
|
||||
- Don't use disabled tests.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
- `npx ultracite init` - Initialize Ultracite in your project
|
||||
- `npx ultracite format` - Format and fix code automatically
|
||||
- `npx ultracite lint` - Check for issues without fixing
|
||||
|
||||
## Example: Error Handling
|
||||
|
||||
```typescript
|
||||
// ✅ Good: Comprehensive error handling
|
||||
try {
|
||||
const result = await fetchData();
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
console.error("API call failed:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
// ❌ Bad: Swallowing errors
|
||||
try {
|
||||
return await fetchData();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
```
|
||||
@@ -1,49 +1,11 @@
|
||||
## Description
|
||||
|
||||
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- [ ] Performance improvement
|
||||
- [ ] Code refactoring
|
||||
- [ ] Tests
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||
|
||||
- [ ] Test A
|
||||
- [ ] Test B
|
||||
|
||||
**Test Configuration**:
|
||||
* Node version:
|
||||
* Browser (if applicable):
|
||||
* Operating System:
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
Add screenshots to help explain your changes.
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have added screenshots if ui has been changed
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the pull request here.
|
||||
⚠️ READ BEFORE SUBMITTING ⚠️
|
||||
|
||||
We are not currently accepting PRs except for critical bugs.
|
||||
|
||||
If this is a bug fix:
|
||||
- [ ] I've opened an issue first
|
||||
- [ ] This was approved by a maintainer
|
||||
|
||||
If this is a feature:
|
||||
|
||||
This PR will be closed. Please open an issue to discuss first.
|
||||
@@ -1,57 +1,37 @@
|
||||
name: Bun CI
|
||||
|
||||
concurrency:
|
||||
group: bun-ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
env:
|
||||
DATABASE_URL: "postgresql://opencut: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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76
|
||||
with:
|
||||
bun-version: 1.2.18
|
||||
|
||||
- name: Cache Bun modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: ${{ runner.os }}-bun-1.2.18-${{ hashFiles('apps/web/bun.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: apps/web
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
working-directory: apps/web
|
||||
run: bun run build
|
||||
|
||||
- name: Run tests
|
||||
working-directory: apps/web
|
||||
run: echo "No tests implemented yet"
|
||||
continue-on-error: true
|
||||
name: Bun CI
|
||||
|
||||
concurrency:
|
||||
group: bun-ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup toolchain
|
||||
uses: moonrepo/setup-toolchain@v0
|
||||
with:
|
||||
auto-install: true
|
||||
auto-setup: true
|
||||
|
||||
- name: Run CI
|
||||
run: moon ci
|
||||
|
||||
@@ -1,35 +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
|
||||
/apps/web/node_modules
|
||||
|
||||
# next.js
|
||||
/apps/web/.next/
|
||||
/apps/web/out/
|
||||
|
||||
# debug
|
||||
/apps/web/npm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
/apps/web/.env*
|
||||
!/apps/web/.env.example
|
||||
|
||||
# typescript
|
||||
/apps/web/next-env.d.ts
|
||||
|
||||
# asdf version management
|
||||
.tool-versions
|
||||
|
||||
node_modules
|
||||
.cursorignore
|
||||
.turbo
|
||||
|
||||
*.env
|
||||
|
||||
# cursor
|
||||
|
||||
.cursor/
|
||||
bun.lockb
|
||||
|
||||
# mac
|
||||
.DS_Store
|
||||
# moon runtime cache
|
||||
.moon/cache/
|
||||
|
||||
@@ -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/**
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[javascript][typescript][javascriptreact][typescriptreact][json][jsonc][css][graphql]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"emmet.showExpandedAbbreviation": "never",
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -1,177 +1,64 @@
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<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 align="right">
|
||||
<h1>OpenCut <span style="font-size: 0.7em; font-weight: normal;">(prev AppCut)</span></h1>
|
||||
<h3 style="margin-top: -10px;">A free, open-source video editor for web, desktop, and mobile.</h3>
|
||||
<h1>OpenCut</h1>
|
||||
<h3 style="margin-top: -10px;">A free and open source video editor for web, desktop, and mobile.</h3>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Why?
|
||||
[](https://discord.gg/zmR9N35cjK)
|
||||
[](https://x.com/opencutapp)
|
||||
[](LICENSE)
|
||||
|
||||
- **Privacy**: Your videos stay on your device
|
||||
- **Free features**: Every basic feature of CapCut is paywalled now
|
||||
- **Simple**: People want editors that are easy to use - CapCut proved that
|
||||
## Status
|
||||
|
||||
## Features
|
||||
**OpenCut is being rewritten from the ground up.** What's coming:
|
||||
|
||||
- Timeline-based editing
|
||||
- Multi-track support
|
||||
- Real-time preview
|
||||
- No watermarks or subscriptions
|
||||
- Analytics provided by [Databuddy](https://www.databuddy.cc?utm_source=opencut), 100% Anonymized & Non-invasive.
|
||||
- Blog powered by [Marble](https://marblecms.com?utm_source=opencut), Headless CMS.
|
||||
- An Editor API
|
||||
- First-class third party plugins (made possible by a plugin-first architecture)
|
||||
- Desktop, mobile, and browser from one codebase (Rust core)
|
||||
- MCP server (for AI agents)
|
||||
- Headless mode (automation, batch rendering)
|
||||
- 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
|
||||
- `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
|
||||
## Development
|
||||
|
||||
## 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:
|
||||
From the repo root:
|
||||
|
||||
- [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/)
|
||||
```sh
|
||||
proto use # installs the tools pinned in .prototools
|
||||
```
|
||||
|
||||
> **Note:** Docker is optional, but it's essential for running the local database and Redis services. If you're planning to run the frontend or want to contribute to frontend features, you can skip the Docker setup. If you have followed the steps below in [Setup](#setup), you're all set to go!
|
||||
|
||||
### Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork locally
|
||||
3. Navigate to the web app directory: `cd apps/web`
|
||||
4. Copy `.env.example` to `.env.local`:
|
||||
|
||||
```bash
|
||||
# Unix/Linux/Mac
|
||||
cp .env.example .env.local
|
||||
|
||||
# Windows Command Prompt
|
||||
copy .env.example .env.local
|
||||
|
||||
# Windows PowerShell
|
||||
Copy-Item .env.example .env.local
|
||||
```
|
||||
|
||||
5. Install dependencies: `bun install`
|
||||
6. Start the development server: `bun dev`
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Local Development
|
||||
|
||||
1. Start the database and Redis services:
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. Navigate to the web app directory:
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
```
|
||||
|
||||
3. Copy `.env.example` to `.env.local`:
|
||||
|
||||
```bash
|
||||
# Unix/Linux/Mac
|
||||
cp .env.example .env.local
|
||||
|
||||
# Windows Command Prompt
|
||||
copy .env.example .env.local
|
||||
|
||||
# Windows PowerShell
|
||||
Copy-Item .env.example .env.local
|
||||
```
|
||||
|
||||
4. Configure required environment variables in `.env.local`:
|
||||
|
||||
**Required Variables:**
|
||||
|
||||
```bash
|
||||
# Database (matches docker-compose.yaml)
|
||||
DATABASE_URL="postgresql://opencut: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).
|
||||
```sh
|
||||
moon run web:dev # localhost:5173
|
||||
moon run api:dev # localhost:8787
|
||||
moon run desktop:dev # see apps/desktop/README.md
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively.
|
||||
|
||||
**🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel.
|
||||
|
||||
**⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach.
|
||||
|
||||
See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance.
|
||||
|
||||
**Quick start for contributors:**
|
||||
|
||||
- Fork the repo and clone locally
|
||||
- Follow the setup instructions in CONTRIBUTING.md
|
||||
- Create a feature branch and submit a PR
|
||||
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).
|
||||
|
||||
## 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">
|
||||
<img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
|
||||
</a>
|
||||
- [**fal.ai**](https://fal.ai?utm_source=github-opencut&utm_campaign=oss): Generative image, video, and audio models all in one place.
|
||||
|
||||
[](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
|
||||
|
||||
[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");
|
||||
});
|
||||
}
|
||||
@@ -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,19 +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
|
||||
@@ -1,2 +1,13 @@
|
||||
# Turborepo
|
||||
.turbo
|
||||
node_modules
|
||||
.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,57 +0,0 @@
|
||||
FROM oven/bun:alpine AS base
|
||||
|
||||
# Install dependencies and build the application
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package.json
|
||||
COPY bun.lock bun.lock
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
COPY apps/web/package.json apps/web/package.json
|
||||
COPY packages/db/package.json packages/db/package.json
|
||||
COPY packages/auth/package.json packages/auth/package.json
|
||||
|
||||
RUN bun install
|
||||
|
||||
COPY apps/web/ apps/web/
|
||||
COPY packages/db/ packages/db/
|
||||
COPY packages/auth/ packages/auth/
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
# Set build-time environment variables for validation
|
||||
ENV DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut"
|
||||
ENV BETTER_AUTH_SECRET="build-time-secret"
|
||||
ENV UPSTASH_REDIS_REST_URL="http://localhost:8079"
|
||||
ENV UPSTASH_REDIS_REST_TOKEN="example_token"
|
||||
ENV NEXT_PUBLIC_BETTER_AUTH_URL="http://localhost:3000"
|
||||
|
||||
WORKDIR /app/apps/web
|
||||
RUN bun run build
|
||||
|
||||
# Production image
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
|
||||
RUN chown nextjs:nodejs apps
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["bun", "apps/web/server.js"]
|
||||
@@ -1,21 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"style": "base-mira",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"config": "",
|
||||
"css": "src/styles.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "hugeicons",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
"components": "#/components",
|
||||
"utils": "#/lib/utils",
|
||||
"ui": "#/components/ui",
|
||||
"lib": "#/lib",
|
||||
"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,319 +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,358 +0,0 @@
|
||||
{
|
||||
"id": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f",
|
||||
"prevId": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.accounts": {
|
||||
"name": "accounts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"accounts_user_id_users_id_fk": {
|
||||
"name": "accounts_user_id_users_id_fk",
|
||||
"tableFrom": "accounts",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.sessions": {
|
||||
"name": "sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_user_id_users_id_fk": {
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"sessions_token_unique": {
|
||||
"name": "sessions_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.verifications": {
|
||||
"name": "verifications",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.waitlist": {
|
||||
"name": "waitlist",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"waitlist_email_unique": {
|
||||
"name": "waitlist_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
{
|
||||
"id": "1b9148ed-497b-4e53-8cf6-f556c8fe7f7f",
|
||||
"prevId": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.accounts": {
|
||||
"name": "accounts",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"accounts_user_id_users_id_fk": {
|
||||
"name": "accounts_user_id_users_id_fk",
|
||||
"tableFrom": "accounts",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.sessions": {
|
||||
"name": "sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_user_id_users_id_fk": {
|
||||
"name": "sessions_user_id_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"sessions_token_unique": {
|
||||
"name": "sessions_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.verifications": {
|
||||
"name": "verifications",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
},
|
||||
"public.waitlist": {
|
||||
"name": "waitlist",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"waitlist_email_unique": {
|
||||
"name": "waitlist_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": true
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,37 +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",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default withBotId(nextConfig);
|
||||
@@ -1,83 +1,73 @@
|
||||
{
|
||||
"name": "opencut",
|
||||
"version": "0.1.0",
|
||||
"name": "@opencut/web",
|
||||
"private": true,
|
||||
"packageManager": "bun@1.2.18",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"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"
|
||||
"dev": "vite dev --port 5173",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"deploy": "bun run build && wrangler deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg/core": "^0.12.10",
|
||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@opencut/auth": "workspace:*",
|
||||
"@opencut/db": "workspace:*",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"@upstash/ratelimit": "^2.0.5",
|
||||
"@upstash/redis": "^1.35.0",
|
||||
"@vercel/analytics": "^1.4.1",
|
||||
"better-auth": "^1.2.7",
|
||||
"botid": "^1.4.2",
|
||||
"@base-ui/react": "^1.4.1",
|
||||
"@cloudflare/vite-plugin": "^1.26.0",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@hugeicons/core-free-icons": "^4.1.2",
|
||||
"@hugeicons/react": "^1.1.6",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "latest",
|
||||
"@tanstack/react-router": "latest",
|
||||
"@tanstack/react-router-devtools": "latest",
|
||||
"@tanstack/react-router-ssr-query": "latest",
|
||||
"@tanstack/react-start": "latest",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"embla-carousel-react": "^8.5.1",
|
||||
"framer-motion": "^11.13.1",
|
||||
"input-otp": "^1.4.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"motion": "^12.18.1",
|
||||
"next": "^15.4.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"pg": "^8.16.2",
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-phone-number-input": "^3.4.11",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.14.1",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"sonner": "^1.7.1",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unified": "^11.0.5",
|
||||
"vaul": "^1.1.1",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.2"
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^1.14.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^10.0.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.75.0",
|
||||
"react-resizable-panels": "^4.11.0",
|
||||
"recharts": "3.8.0",
|
||||
"shadcn": "^4.7.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/bun": "latest",
|
||||
"@types/pg": "^8.15.4",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"cross-env": "^7.0.3",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.8.3"
|
||||
"@tanstack/devtools-vite": "latest",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"jsdom": "^28.1.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.0",
|
||||
"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: {},
|
||||
},
|
||||
};
|
||||
|
||||
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: 467 B |
|
Before Width: | Height: | Size: 564 B |
|
Before Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 253 B |
|
Before Width: | Height: | Size: 274 B |
|
Before Width: | Height: | Size: 334 B |
|
Before Width: | Height: | Size: 399 B |
|
Before Width: | Height: | Size: 419 B |
|
Before Width: | Height: | Size: 467 B |
|
Before Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 724 B |
|
Before Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 274 B |
|
Before Width: | Height: | Size: 263 B |
|
Before Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 206 B |
|
Before Width: | Height: | Size: 334 B |
|
Before Width: | Height: | Size: 467 B |
|
Before Width: | Height: | Size: 539 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 259 B |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 1021 B |
@@ -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",
|
||||
"description": "A simple but powerful video editor that gets the job done. In your browser.",
|
||||
"display": "standalone",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image\/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image\/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image\/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image\/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image\/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "/icons/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image\/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
"short_name": "TanStack App",
|
||||
"name": "Create TanStack App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 56 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,4 +0,0 @@
|
||||
import { auth } from "@opencut/auth";
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
export const { POST, GET } = toNextJsHandler(auth);
|
||||
@@ -1,5 +0,0 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return new Response("OK", { status: 200 });
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db, eq, waitlist } from "@opencut/db";
|
||||
import { checkBotId } from "botid/server";
|
||||
import { nanoid } from "nanoid";
|
||||
import { waitlistRateLimit } from "@/lib/rate-limit";
|
||||
import { z } from "zod";
|
||||
import { env } from "@/env";
|
||||
import { cookies } from "next/headers";
|
||||
import crypto from "crypto";
|
||||
|
||||
const waitlistSchema = z.object({
|
||||
email: z.string().email("Invalid email format").min(1, "Email is required"),
|
||||
});
|
||||
|
||||
const CSRF_TOKEN_NAME = "waitlist-csrf";
|
||||
const TOKEN_EXPIRY = 60 * 60 * 1000;
|
||||
|
||||
async function validateCSRFToken(request: NextRequest): Promise<boolean> {
|
||||
const clientToken = request.headers.get("x-csrf-token");
|
||||
if (!clientToken) return false;
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const cookieValue = cookieStore.get(CSRF_TOKEN_NAME)?.value;
|
||||
if (!cookieValue) return false;
|
||||
|
||||
const [token, timestamp, signature] = cookieValue.split(":");
|
||||
if (!token || !timestamp || !signature) return false;
|
||||
|
||||
if (clientToken !== token) return false;
|
||||
|
||||
const now = Date.now();
|
||||
const tokenTime = parseInt(timestamp);
|
||||
if (now - tokenTime > TOKEN_EXPIRY) return false;
|
||||
|
||||
const expectedSignature = crypto.createHmac("sha256", env.BETTER_AUTH_SECRET).update(`${token}:${timestamp}`).digest("hex");
|
||||
|
||||
return signature === expectedSignature;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const verification = await checkBotId();
|
||||
|
||||
if (verification.isBot) {
|
||||
return NextResponse.json({ error: "Access denied" }, { status: 403 });
|
||||
}
|
||||
|
||||
const identifier = request.headers.get("x-forwarded-for") ?? "127.0.0.1";
|
||||
const { success } = await waitlistRateLimit.limit(identifier);
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json({ error: "Too many requests. Please try again later." }, { status: 429 });
|
||||
}
|
||||
const isValidToken = await validateCSRFToken(request);
|
||||
if (!isValidToken) {
|
||||
return NextResponse.json({ error: "Invalid security token" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email } = waitlistSchema.parse(body);
|
||||
|
||||
const existingEmail = await db.select().from(waitlist).where(eq(waitlist.email, email.toLowerCase())).limit(1);
|
||||
|
||||
if (existingEmail.length > 0) {
|
||||
return NextResponse.json({ error: "Email already registered" }, { status: 409 });
|
||||
}
|
||||
|
||||
await db.insert(waitlist).values({
|
||||
id: nanoid(),
|
||||
email: email.toLowerCase(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: "Successfully joined waitlist!" }, { status: 201 });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const firstError = error.errors[0];
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 });
|
||||
}
|
||||
|
||||
console.error("Waitlist signup error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import crypto from "crypto";
|
||||
import { env } from "@/env";
|
||||
|
||||
const CSRF_TOKEN_NAME = "waitlist-csrf";
|
||||
const TOKEN_EXPIRY = 60 * 60 * 1000;
|
||||
const allowedHosts = env.NODE_ENV === "development" ? ["localhost:3000", "127.0.0.1:3000"] : ["opencut.app", "www.opencut.app"];
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const referer = request.headers.get("referer");
|
||||
const host = request.headers.get("host");
|
||||
|
||||
if (referer) {
|
||||
const refererUrl = new URL(referer);
|
||||
|
||||
if (!allowedHosts.some((allowed) => refererUrl.host === allowed || refererUrl.host.endsWith(allowed))) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
} else if (host) {
|
||||
if (!allowedHosts.some((allowed) => host === allowed || host.endsWith(allowed))) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!env.BETTER_AUTH_SECRET) {
|
||||
throw new Error("BETTER_AUTH_SECRET must be configured");
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
const timestamp = Date.now();
|
||||
const signature = crypto.createHmac("sha256", env.BETTER_AUTH_SECRET).update(`${token}:${timestamp}`).digest("hex");
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(CSRF_TOKEN_NAME, `${token}:${timestamp}:${signature}`, {
|
||||
httpOnly: true,
|
||||
secure: env.NODE_ENV === "production",
|
||||
sameSite: "strict",
|
||||
maxAge: TOKEN_EXPIRY / 1000,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return NextResponse.json({ token });
|
||||
}
|
||||
@@ -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-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-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-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-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>
|
||||
);
|
||||
}
|
||||
@@ -1,277 +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 { Button } from "@/components/ui/button";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { GithubIcon } from "@/components/icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contributors - OpenCut",
|
||||
description:
|
||||
"Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
|
||||
openGraph: {
|
||||
title: "Contributors - OpenCut",
|
||||
description:
|
||||
"Meet the amazing people who contribute to OpenCut, the free and open-source video editor.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
interface Contributor {
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
contributions: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
async function getContributors(): Promise<Contributor[]> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/OpenCut-app/OpenCut/contributors?per_page=100",
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenCut-Web-App",
|
||||
},
|
||||
next: { revalidate: 600 }, // 10 minutes
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch contributors");
|
||||
return [];
|
||||
}
|
||||
|
||||
const contributors = (await response.json()) as Contributor[];
|
||||
|
||||
const filteredContributors = contributors.filter(
|
||||
(contributor: Contributor) => contributor.type === "User"
|
||||
);
|
||||
|
||||
return filteredContributors;
|
||||
} catch (error) {
|
||||
console.error("Error fetching contributors:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ContributorsPage() {
|
||||
const contributors = await getContributors();
|
||||
const topContributors = contributors.slice(0, 2);
|
||||
const otherContributors = contributors.slice(2);
|
||||
|
||||
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-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative container mx-auto px-4 py-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-20">
|
||||
<Link
|
||||
href={"https://github.com/OpenCut-app/OpenCut"}
|
||||
target="_blank"
|
||||
>
|
||||
<Badge variant="secondary" className="gap-2 mb-6">
|
||||
<GithubIcon className="h-3 w-3" />
|
||||
Open Source
|
||||
</Badge>
|
||||
</Link>
|
||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
|
||||
Contributors
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
|
||||
Meet the amazing developers who are building the future of video
|
||||
editing
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-center gap-8 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-foreground rounded-full" />
|
||||
<span className="font-medium">{contributors.length}</span>
|
||||
<span className="text-muted-foreground">contributors</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-foreground rounded-full" />
|
||||
<span className="font-medium">
|
||||
{contributors.reduce((sum, c) => sum + c.contributions, 0)}
|
||||
</span>
|
||||
<span className="text-muted-foreground">contributions</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{topContributors.length > 0 && (
|
||||
<div className="mb-20">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl font-semibold mb-2">
|
||||
Top Contributors
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Leading the way in contributions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 justify-center max-w-4xl mx-auto">
|
||||
{topContributors.map((contributor, index) => (
|
||||
<Link
|
||||
key={contributor.id}
|
||||
href={contributor.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group block flex-1"
|
||||
>
|
||||
<div className="relative mx-auto max-w-md">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-muted/50 to-muted/30 rounded-2xl blur group-hover:blur-md transition-all duration-300" />
|
||||
<Card className="relative bg-background/80 backdrop-blur-sm border-2 group-hover:border-muted-foreground/20 transition-all duration-300 group-hover:shadow-xl">
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="relative mb-6">
|
||||
<Avatar className="h-24 w-24 mx-auto ring-4 ring-background shadow-2xl">
|
||||
<AvatarImage
|
||||
src={contributor.avatar_url}
|
||||
alt={`${contributor.login}'s avatar`}
|
||||
/>
|
||||
<AvatarFallback className="text-lg font-semibold">
|
||||
{contributor.login.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2 group-hover:text-foreground/80 transition-colors">
|
||||
{contributor.login}
|
||||
</h3>
|
||||
<div className="flex items-center justify-center gap-2 text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{contributor.contributions}
|
||||
</span>
|
||||
<span>contributions</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{otherContributors.length > 0 && (
|
||||
<div>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-2xl font-semibold mb-2">
|
||||
All Contributors
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Everyone who makes OpenCut better
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||
{otherContributors.map((contributor, index) => (
|
||||
<Link
|
||||
key={contributor.id}
|
||||
href={contributor.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group block"
|
||||
style={{
|
||||
animationDelay: `${index * 50}ms`,
|
||||
}}
|
||||
>
|
||||
<div className="text-center p-2 rounded-xl transition-all duration-300 hover:opacity-50">
|
||||
<Avatar className="h-16 w-16 mx-auto mb-3">
|
||||
<AvatarImage
|
||||
src={contributor.avatar_url}
|
||||
alt={`${contributor.login}'s avatar`}
|
||||
/>
|
||||
<AvatarFallback className="font-medium">
|
||||
{contributor.login.charAt(0).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<h3 className="font-medium text-sm truncate group-hover:text-foreground transition-colors mb-1">
|
||||
{contributor.login}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{contributor.contributions}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contributors.length === 0 && (
|
||||
<div className="text-center py-20">
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-muted/50 flex items-center justify-center">
|
||||
<GithubIcon className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-3">
|
||||
No contributors found
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-8 max-w-md mx-auto">
|
||||
Unable to load contributors at the moment. Check back later or
|
||||
view on GitHub.
|
||||
</p>
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut/graphs/contributors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<GithubIcon className="h-4 w-4" />
|
||||
View on GitHub
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-32 text-center">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h2 className="text-3xl font-bold mb-4">Join the community</h2>
|
||||
<p className="text-lg text-muted-foreground mb-10 leading-relaxed">
|
||||
OpenCut is built by developers like you. Every contribution,
|
||||
no matter how small, helps make video editing more accessible
|
||||
for everyone.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut/blob/main/.github/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button size="lg" className="gap-2 group">
|
||||
<GithubIcon className="h-4 w-4 group-hover:scale-110 transition-transform" />
|
||||
Start Contributing
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button variant="outline" size="lg" className="gap-2 group">
|
||||
Browse Issues
|
||||
<ExternalLink className="h-4 w-4 group-hover:scale-110 transition-transform" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
ResizablePanelGroup,
|
||||
ResizablePanel,
|
||||
ResizableHandle,
|
||||
} from "../../../components/ui/resizable";
|
||||
import { MediaPanel } from "../../../components/editor/media-panel";
|
||||
import { PropertiesPanel } from "../../../components/editor/properties-panel";
|
||||
import { Timeline } from "../../../components/editor/timeline";
|
||||
import { PreviewPanel } from "../../../components/editor/preview-panel";
|
||||
import { EditorHeader } from "@/components/editor-header";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import { useProjectStore } from "@/stores/project-store";
|
||||
import { EditorProvider } from "@/components/editor-provider";
|
||||
import { usePlaybackControls } from "@/hooks/use-playback-controls";
|
||||
import { Onboarding } from "@/components/onboarding";
|
||||
|
||||
export default function Editor() {
|
||||
const {
|
||||
toolsPanel,
|
||||
previewPanel,
|
||||
mainContent,
|
||||
timeline,
|
||||
setToolsPanel,
|
||||
setPreviewPanel,
|
||||
setMainContent,
|
||||
setTimeline,
|
||||
propertiesPanel,
|
||||
setPropertiesPanel,
|
||||
} = usePanelStore();
|
||||
|
||||
const { activeProject, loadProject, createNewProject } = useProjectStore();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const projectId = params.project_id as string;
|
||||
const handledProjectIds = useRef<Set<string>>(new Set());
|
||||
|
||||
usePlaybackControls();
|
||||
|
||||
useEffect(() => {
|
||||
const initProject = async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
if (activeProject?.id === projectId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handledProjectIds.current.has(projectId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await loadProject(projectId);
|
||||
} catch (error) {
|
||||
handledProjectIds.current.add(projectId);
|
||||
|
||||
const newProjectId = await createNewProject("Untitled Project");
|
||||
router.replace(`/editor/${newProjectId}`);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
initProject();
|
||||
}, [projectId, activeProject?.id, loadProject, createNewProject, router]);
|
||||
|
||||
return (
|
||||
<EditorProvider>
|
||||
<div className="h-screen w-screen flex flex-col bg-background overflow-hidden">
|
||||
<EditorHeader />
|
||||
<div className="flex-1 min-h-0 min-w-0">
|
||||
<ResizablePanelGroup
|
||||
direction="vertical"
|
||||
className="h-full w-full gap-[0.18rem]"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={mainContent}
|
||||
minSize={30}
|
||||
maxSize={85}
|
||||
onResize={setMainContent}
|
||||
className="min-h-0"
|
||||
>
|
||||
{/* Main content area */}
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="h-full w-full gap-[0.19rem] px-2"
|
||||
>
|
||||
{/* Tools Panel */}
|
||||
<ResizablePanel
|
||||
defaultSize={toolsPanel}
|
||||
minSize={15}
|
||||
maxSize={40}
|
||||
onResize={setToolsPanel}
|
||||
className="min-w-0"
|
||||
>
|
||||
<MediaPanel />
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
{/* Preview Area */}
|
||||
<ResizablePanel
|
||||
defaultSize={previewPanel}
|
||||
minSize={30}
|
||||
onResize={setPreviewPanel}
|
||||
className="min-w-0 min-h-0 flex-1"
|
||||
>
|
||||
<PreviewPanel />
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
<ResizablePanel
|
||||
defaultSize={propertiesPanel}
|
||||
minSize={15}
|
||||
maxSize={40}
|
||||
onResize={setPropertiesPanel}
|
||||
className="min-w-0"
|
||||
>
|
||||
<PropertiesPanel />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
{/* Timeline */}
|
||||
<ResizablePanel
|
||||
defaultSize={timeline}
|
||||
minSize={15}
|
||||
maxSize={70}
|
||||
onResize={setTimeline}
|
||||
className="min-h-0 px-2 pb-2"
|
||||
>
|
||||
<Timeline />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
<Onboarding />
|
||||
</div>
|
||||
</EditorProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 1rem;
|
||||
--sidebar-background: 0 0% 100%;
|
||||
--sidebar-foreground: 0 0% 3.9%;
|
||||
--sidebar-primary: 0 0% 9%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 0 0% 96.1%;
|
||||
--sidebar-accent-foreground: 0 0% 9%;
|
||||
--sidebar-border: 0 0% 89.8%;
|
||||
--sidebar-ring: 0 0% 3.9%;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 4%;
|
||||
--foreground: 0 0% 89%;
|
||||
--card: 0 0% 14.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 14.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 180 95% 40%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 100% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 17%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 0 0% 3.9%;
|
||||
--sidebar-foreground: 0 0% 98%;
|
||||
--sidebar-primary: 0 0% 98%;
|
||||
--sidebar-primary-foreground: 0 0% 9%;
|
||||
--sidebar-accent: 0 0% 14.9%;
|
||||
--sidebar-accent-foreground: 0 0% 98%;
|
||||
--sidebar-border: 0 0% 14.9%;
|
||||
--sidebar-ring: 0 0% 83.1%;
|
||||
--panel-background: 0 0% 11%;
|
||||
--panel-accent: 0 0% 15%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
/* Prevent back/forward swipe */
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import Script from "next/script";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "../components/ui/sonner";
|
||||
import { TooltipProvider } from "../components/ui/tooltip";
|
||||
import { StorageProvider } from "../components/storage-provider";
|
||||
import { baseMetaData } from "./metadata";
|
||||
import { defaultFont } from "../lib/font-config";
|
||||
import { BotIdClient } from "botid/client";
|
||||
import { env } from "@/env";
|
||||
|
||||
export const metadata = baseMetaData;
|
||||
|
||||
const protectedRoutes = [
|
||||
{
|
||||
path: "/api/waitlist",
|
||||
method: "POST",
|
||||
},
|
||||
];
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<BotIdClient protect={protectedRoutes} />
|
||||
</head>
|
||||
<body className={`${defaultFont.className} font-sans antialiased`}>
|
||||
<ThemeProvider attribute="class" forcedTheme="dark">
|
||||
<TooltipProvider>
|
||||
<StorageProvider>{children}</StorageProvider>
|
||||
<Analytics />
|
||||
<Toaster />
|
||||
<Script
|
||||
src="https://cdn.databuddy.cc/databuddy.js"
|
||||
strategy="afterInteractive"
|
||||
async
|
||||
data-client-id="UP-Wcoy5arxFeK7oyjMMZ"
|
||||
data-disabled={env.NODE_ENV === "development"}
|
||||
data-track-attributes={false}
|
||||
data-track-errors={true}
|
||||
data-track-outgoing-links={false}
|
||||
data-track-web-vitals={false}
|
||||
data-track-sessions={false}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
|
||||
const title = "OpenCut";
|
||||
const description =
|
||||
"A simple but powerful video editor that gets the job done. In your browser.";
|
||||
const openGraphImageUrl = "https://opencut.app/open-graph/default.jpg";
|
||||
const twitterImageUrl = "/open-graph/default.jpg";
|
||||
|
||||
export const baseMetaData: Metadata = {
|
||||
metadataBase: new URL("https://opencut.app"),
|
||||
title: title,
|
||||
description: description,
|
||||
openGraph: {
|
||||
title: title,
|
||||
description: description,
|
||||
url: "https://opencut.app",
|
||||
siteName: "OpenCut",
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: openGraphImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "OpenCut",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: title,
|
||||
description: description,
|
||||
creator: "@opencutapp",
|
||||
images: [twitterImageUrl],
|
||||
},
|
||||
pinterest: {
|
||||
richPin: false,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.ico" },
|
||||
{ url: "/icons/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
{ url: "/icons/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
{ url: "/icons/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
||||
],
|
||||
apple: [
|
||||
{ url: "/icons/apple-icon-57x57.png", sizes: "57x57", type: "image/png" },
|
||||
{ url: "/icons/apple-icon-60x60.png", sizes: "60x60", type: "image/png" },
|
||||
{ url: "/icons/apple-icon-72x72.png", sizes: "72x72", type: "image/png" },
|
||||
{ url: "/icons/apple-icon-76x76.png", sizes: "76x76", type: "image/png" },
|
||||
{
|
||||
url: "/icons/apple-icon-114x114.png",
|
||||
sizes: "114x114",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
url: "/icons/apple-icon-120x120.png",
|
||||
sizes: "120x120",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
url: "/icons/apple-icon-144x144.png",
|
||||
sizes: "144x144",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
url: "/icons/apple-icon-152x152.png",
|
||||
sizes: "152x152",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
url: "/icons/apple-icon-180x180.png",
|
||||
sizes: "180x180",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
shortcut: ["/favicon.ico"],
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
title: title,
|
||||
},
|
||||
manifest: "/manifest.json",
|
||||
other: {
|
||||
"msapplication-config": "/browserconfig.xml",
|
||||
},
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Hero } from "@/components/landing/hero";
|
||||
import { Header } from "@/components/header";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Hero />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { GithubIcon } from "@/components/icons";
|
||||
import Link from "next/link";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy Policy - OpenCut",
|
||||
description:
|
||||
"Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.",
|
||||
openGraph: {
|
||||
title: "Privacy Policy - OpenCut",
|
||||
description:
|
||||
"Learn how OpenCut handles your data and privacy. Our commitment to protecting your information while you edit videos.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
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-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
|
||||
</div>
|
||||
<div className="relative container mx-auto px-4 py-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-10">
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut"
|
||||
target="_blank"
|
||||
>
|
||||
<Badge variant="secondary" className="gap-2 mb-6">
|
||||
<GithubIcon className="h-3 w-3" />
|
||||
Open Source
|
||||
</Badge>
|
||||
</Link>
|
||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
|
||||
Learn how we handle your data and privacy. Contact us if you
|
||||
have any questions.
|
||||
</p>
|
||||
</div>
|
||||
<Card className="bg-background/80 backdrop-blur-sm border-2 border-muted/30">
|
||||
<CardContent className="p-8 text-base leading-relaxed space-y-8">
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Your Videos Stay Private
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
<strong>
|
||||
OpenCut processes all videos locally on your device.
|
||||
</strong>{" "}
|
||||
We never upload, store, or have access to your video files.
|
||||
Your content remains completely private and under your
|
||||
control at all times.
|
||||
</p>
|
||||
<p>
|
||||
All video editing, rendering, and processing happens in your
|
||||
browser using WebAssembly and local storage. No video data
|
||||
is transmitted to our servers.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Account Information
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
When you create an account, we only collect:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Email address (for account access)</li>
|
||||
<li>
|
||||
Profile information from Google OAuth (if you choose to
|
||||
sign in with Google)
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-4">
|
||||
<strong>
|
||||
We do NOT store your projects on our servers.
|
||||
</strong>{" "}
|
||||
All project data, including names, thumbnails, and creation
|
||||
dates, is stored locally in your browser using IndexedDB.
|
||||
</p>
|
||||
<p>
|
||||
We use{" "}
|
||||
<a
|
||||
href="https://www.better-auth.com"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Better Auth
|
||||
</a>{" "}
|
||||
for secure authentication and follow industry-standard
|
||||
security practices.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">Analytics</h2>
|
||||
<p className="mb-4">
|
||||
We use{" "}
|
||||
<a
|
||||
href="https://www.databuddy.cc"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Databuddy
|
||||
</a>{" "}
|
||||
for completely anonymized and non-invasive analytics to
|
||||
understand how people use OpenCut.
|
||||
</p>
|
||||
<p>
|
||||
This helps us improve the editor, but we never collect
|
||||
personal information, track individual users, or store any
|
||||
data that could identify you.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Local Storage & Cookies
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
We use browser local storage and IndexedDB to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Save your projects locally on your device</li>
|
||||
<li>Remember your editor preferences and settings</li>
|
||||
<li>Keep you logged in across browser sessions</li>
|
||||
</ul>
|
||||
<p>
|
||||
All data stays on your device and can be cleared at any time
|
||||
through your browser settings.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Third-Party Services
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
OpenCut integrates with these services:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>
|
||||
<strong>Google OAuth:</strong> For optional Google sign-in
|
||||
(governed by Google's privacy policy)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Vercel:</strong> For hosting and content delivery
|
||||
</li>
|
||||
<li>
|
||||
<strong>Databuddy:</strong> For anonymized analytics
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">Your Rights</h2>
|
||||
<p className="mb-4">
|
||||
You have complete control over your data:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>
|
||||
Delete your account and all associated data at any time
|
||||
</li>
|
||||
<li>Export your project data</li>
|
||||
<li>Clear local storage to remove all saved projects</li>
|
||||
<li>Contact us with any privacy concerns</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Open Source Transparency
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
OpenCut is completely open source. You can review our code,
|
||||
see exactly how we handle data, and even self-host the
|
||||
application if you prefer.
|
||||
</p>
|
||||
<p>
|
||||
View our source code on{" "}
|
||||
<a
|
||||
href="https://github.com/OpenCut-app/OpenCut"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">Contact Us</h2>
|
||||
<p className="mb-4">
|
||||
Questions about this privacy policy or how we handle your
|
||||
data?
|
||||
</p>
|
||||
<p>
|
||||
Open an issue on our{" "}
|
||||
<a
|
||||
href="https://github.com/OpenCut-app/OpenCut/issues"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
GitHub repository
|
||||
</a>
|
||||
, email us at{" "}
|
||||
<a
|
||||
href="mailto:oss@opencut.app"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
oss@opencut.app
|
||||
</a>
|
||||
, or reach out on{" "}
|
||||
<a
|
||||
href="https://x.com/opencutapp"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
X (Twitter)
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="text-sm text-muted-foreground mt-8 pt-8 border-t border-muted/20">
|
||||
Last updated: July 14, 2025
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,607 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
ChevronLeft,
|
||||
Plus,
|
||||
Calendar,
|
||||
MoreHorizontal,
|
||||
Video,
|
||||
Loader2,
|
||||
X,
|
||||
Trash2,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import { TProject } from "@/types/project";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useProjectStore } from "@/stores/project-store";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { DeleteProjectDialog } from "@/components/delete-project-dialog";
|
||||
import { RenameProjectDialog } from "@/components/rename-project-dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const {
|
||||
createNewProject,
|
||||
savedProjects,
|
||||
isLoading,
|
||||
isInitialized,
|
||||
deleteProject,
|
||||
getFilteredAndSortedProjects,
|
||||
} = useProjectStore();
|
||||
const router = useRouter();
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [sortOption, setSortOption] = useState("createdAt-desc");
|
||||
|
||||
const handleCreateProject = async () => {
|
||||
const projectId = await createNewProject("New Project");
|
||||
console.log("projectId", projectId);
|
||||
router.push(`/editor/${projectId}`);
|
||||
};
|
||||
|
||||
const handleSelectProject = (projectId: string, checked: boolean) => {
|
||||
const newSelected = new Set(selectedProjects);
|
||||
if (checked) {
|
||||
newSelected.add(projectId);
|
||||
} else {
|
||||
newSelected.delete(projectId);
|
||||
}
|
||||
setSelectedProjects(newSelected);
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedProjects(new Set(sortedProjects.map((p) => p.id)));
|
||||
} else {
|
||||
setSelectedProjects(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelSelection = () => {
|
||||
setIsSelectionMode(false);
|
||||
setSelectedProjects(new Set());
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
await Promise.all(
|
||||
Array.from(selectedProjects).map((projectId) => deleteProject(projectId))
|
||||
);
|
||||
setSelectedProjects(new Set());
|
||||
setIsSelectionMode(false);
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
const sortedProjects = getFilteredAndSortedProjects(searchQuery, sortOption);
|
||||
|
||||
const allSelected =
|
||||
sortedProjects.length > 0 &&
|
||||
selectedProjects.size === sortedProjects.length;
|
||||
const someSelected =
|
||||
selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="pt-6 px-6 flex items-center justify-between w-full h-16">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-1 hover:text-muted-foreground transition-colors"
|
||||
>
|
||||
<ChevronLeft className="!size-5 shrink-0" />
|
||||
<span className="text-sm font-medium">Back</span>
|
||||
</Link>
|
||||
<div className="block md:hidden">
|
||||
{isSelectionMode ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCancelSelection}
|
||||
>
|
||||
<X className="!size-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
{selectedProjects.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 className="!size-4" />
|
||||
Delete ({selectedProjects.size})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<CreateButton onClick={handleCreateProject} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<main className="max-w-6xl mx-auto px-6 pt-6 pb-6">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="flex flex-col gap-3">
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
|
||||
Your Projects
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{savedProjects.length}{" "}
|
||||
{savedProjects.length === 1 ? "project" : "projects"}
|
||||
{isSelectionMode && selectedProjects.size > 0 && (
|
||||
<span className="ml-2 text-primary">
|
||||
• {selectedProjects.size} selected
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
{isSelectionMode ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={handleCancelSelection}>
|
||||
<X className="!size-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
{selectedProjects.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
||||
>
|
||||
<Trash2 className="!size-4" />
|
||||
Delete Selected ({selectedProjects.size})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsSelectionMode(true)}
|
||||
disabled={savedProjects.length === 0}
|
||||
>
|
||||
Select Projects
|
||||
</Button>
|
||||
<CreateButton onClick={handleCreateProject} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center justify-between gap-4">
|
||||
<div className="flex-1 max-w-72">
|
||||
<Input
|
||||
placeholder="Search projects..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Select value={sortOption} onValueChange={setSortOption}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="createdAt-desc">Newest to Oldest</SelectItem>
|
||||
<SelectItem value="createdAt-asc">Oldest to Newest</SelectItem>
|
||||
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{isSelectionMode && sortedProjects.length > 0 && (
|
||||
<div className="mb-6 p-4 bg-muted/30 rounded-lg border">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={someSelected ? "indeterminate" : allSelected}
|
||||
onCheckedChange={(value) => handleSelectAll(!!value)}
|
||||
/>
|
||||
<span className="text-sm font-medium">
|
||||
{allSelected ? "Deselect All" : "Select All"}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({selectedProjects.size} of {sortedProjects.length} selected)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading || !isInitialized ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2 className="h-8 w-8 text-muted-foreground animate-spin" />
|
||||
</div>
|
||||
) : savedProjects.length === 0 ? (
|
||||
<NoProjects onCreateProject={handleCreateProject} />
|
||||
) : sortedProjects.length === 0 ? (
|
||||
<NoResults
|
||||
searchQuery={searchQuery}
|
||||
onClearSearch={() => setSearchQuery("")}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{sortedProjects.map((project) => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={selectedProjects.has(project.id)}
|
||||
onSelect={handleSelectProject}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<DeleteProjectDialog
|
||||
isOpen={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
onConfirm={handleBulkDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: TProject;
|
||||
isSelectionMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
onSelect?: (projectId: string, checked: boolean) => void;
|
||||
}
|
||||
|
||||
function ProjectCard({
|
||||
project,
|
||||
isSelectionMode = false,
|
||||
isSelected = false,
|
||||
onSelect,
|
||||
}: ProjectCardProps) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
const { deleteProject, renameProject, duplicateProject } = useProjectStore();
|
||||
|
||||
const formatDate = (date: Date): string => {
|
||||
return date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteProject = async () => {
|
||||
await deleteProject(project.id);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleRenameProject = async (newName: string) => {
|
||||
await renameProject(project.id, newName);
|
||||
setIsRenameDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDuplicateProject = async () => {
|
||||
setIsDropdownOpen(false);
|
||||
await duplicateProject(project.id);
|
||||
};
|
||||
|
||||
const handleCardClick = (e: React.MouseEvent) => {
|
||||
if (isSelectionMode) {
|
||||
e.preventDefault();
|
||||
onSelect?.(project.id, !isSelected);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSelectionMode ? (
|
||||
<div onClick={handleCardClick} className="block group cursor-pointer">
|
||||
<Card
|
||||
className={`overflow-hidden bg-background border-none p-0 transition-all ${
|
||||
isSelectionMode && isSelected ? "ring-2 ring-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`relative aspect-square bg-muted transition-opacity ${
|
||||
isDropdownOpen
|
||||
? "opacity-65"
|
||||
: "opacity-100 group-hover:opacity-65"
|
||||
}`}
|
||||
>
|
||||
{/* Selection checkbox */}
|
||||
{isSelectionMode && (
|
||||
<div className="absolute top-3 left-3 z-10">
|
||||
<div className="w-5 h-5 rounded bg-background/80 backdrop-blur-sm border flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) =>
|
||||
onSelect?.(project.id, checked as boolean)
|
||||
}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Thumbnail preview or placeholder */}
|
||||
<div className="absolute inset-0">
|
||||
{project.thumbnail ? (
|
||||
<Image
|
||||
src={project.thumbnail}
|
||||
alt="Project thumbnail"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-muted/50 flex items-center justify-center">
|
||||
<Video className="h-12 w-12 flex-shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="px-0 pt-5 flex flex-col gap-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2">
|
||||
{project.name}
|
||||
</h3>
|
||||
{!isSelectionMode && (
|
||||
<DropdownMenu
|
||||
open={isDropdownOpen}
|
||||
onOpenChange={setIsDropdownOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
className={`size-6 p-0 transition-all shrink-0 ml-2 ${
|
||||
isDropdownOpen
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100"
|
||||
}`}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onCloseAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDropdownOpen(false);
|
||||
setIsRenameDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDuplicateProject();
|
||||
}}
|
||||
>
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDropdownOpen(false);
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Calendar className="!size-4" />
|
||||
<span>Created {formatDate(project.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/editor/${project.id}`} className="block group">
|
||||
<Card
|
||||
className={`overflow-hidden bg-background border-none p-0 transition-all ${
|
||||
isSelectionMode && isSelected ? "ring-2 ring-primary" : ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`relative aspect-square bg-muted transition-opacity ${
|
||||
isDropdownOpen
|
||||
? "opacity-65"
|
||||
: "opacity-100 group-hover:opacity-65"
|
||||
}`}
|
||||
>
|
||||
{/* Thumbnail preview or placeholder */}
|
||||
<div className="absolute inset-0">
|
||||
{project.thumbnail ? (
|
||||
<Image
|
||||
src={project.thumbnail}
|
||||
alt="Project thumbnail"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-muted/50 flex items-center justify-center">
|
||||
<Video className="h-12 w-12 flex-shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="px-0 pt-5 flex flex-col gap-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2">
|
||||
{project.name}
|
||||
</h3>
|
||||
<DropdownMenu
|
||||
open={isDropdownOpen}
|
||||
onOpenChange={setIsDropdownOpen}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
className={`size-6 p-0 transition-all shrink-0 ml-2 ${
|
||||
isDropdownOpen
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:opacity-100"
|
||||
}`}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onCloseAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDropdownOpen(false);
|
||||
setIsRenameDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDuplicateProject();
|
||||
}}
|
||||
>
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDropdownOpen(false);
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Calendar className="!size-4" />
|
||||
<span>Created {formatDate(project.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)}
|
||||
<DeleteProjectDialog
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
onConfirm={handleDeleteProject}
|
||||
projectName={project.name}
|
||||
/>
|
||||
<RenameProjectDialog
|
||||
isOpen={isRenameDialogOpen}
|
||||
onOpenChange={setIsRenameDialogOpen}
|
||||
onConfirm={handleRenameProject}
|
||||
projectName={project.name}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateButton({ onClick }: { onClick?: () => void }) {
|
||||
return (
|
||||
<Button className="flex" onClick={onClick}>
|
||||
<Plus className="!size-4" />
|
||||
<span className="text-sm font-medium">New project</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function NoProjects({ onCreateProject }: { onCreateProject: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
|
||||
<Video className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">No projects yet</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-md">
|
||||
Start creating your first video project. Import media, edit, and export
|
||||
professional videos.
|
||||
</p>
|
||||
<Button size="lg" className="gap-2" onClick={onCreateProject}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Your First Project
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoResults({
|
||||
searchQuery,
|
||||
onClearSearch,
|
||||
}: {
|
||||
searchQuery: string;
|
||||
onClearSearch: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
|
||||
<Search className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium mb-2">No results found</h3>
|
||||
<p className="text-muted-foreground mb-6 max-w-md">
|
||||
Your search for "{searchQuery}" did not return any results.
|
||||
</p>
|
||||
<Button onClick={onClearSearch} variant="outline">
|
||||
Clear Search
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { GithubIcon } from "@/components/icons";
|
||||
import Link from "next/link";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Header } from "@/components/header";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const roadmapItems: {
|
||||
title: string;
|
||||
description: string;
|
||||
status: {
|
||||
text: string;
|
||||
type: "complete" | "pending" | "default" | "info";
|
||||
};
|
||||
}[] = [
|
||||
{
|
||||
title: "Start",
|
||||
description:
|
||||
"This is where it all started. Repository created, initial project structure, and the vision for a free, open-source video editor. [Check out the first tweet](https://x.com/mazeincoding/status/1936706642512388188) to see where it started.",
|
||||
status: {
|
||||
text: "Completed",
|
||||
type: "complete",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Core UI",
|
||||
description:
|
||||
"Built the foundation - main layout, header, sidebar, timeline container, and basic component structure. Not all functionality yet, but the UI framework that everything else builds on.",
|
||||
status: {
|
||||
text: "Completed",
|
||||
type: "complete",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Basic Functionality",
|
||||
description:
|
||||
"The heart of any video editor. Timeline zoom in/out, making clips longer/shorter, dragging elements around, selection, playhead scrubbing. **This part has to be fucking perfect** because it's what users interact with 99% of the time.",
|
||||
status: {
|
||||
text: "In Progress",
|
||||
type: "pending",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Export/Preview Logic",
|
||||
description:
|
||||
"The foundation that enables everything else. Real-time preview, video rendering, export functionality. Once this works, we can add effects, filters, transitions - basically everything that makes a video editor powerful.",
|
||||
status: {
|
||||
text: "In Progress",
|
||||
type: "pending",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Text",
|
||||
description:
|
||||
"After media, text is the next most important thing. Font selection with custom font imports, text stroke, colors. All the text essential text properties.",
|
||||
status: {
|
||||
text: "Not Started",
|
||||
type: "default",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Effects",
|
||||
description:
|
||||
"Adding visual effects to both text and media. Blur, brightness, contrast, saturation, filters, and all the creative tools that make videos pop. This is where the magic happens.",
|
||||
status: {
|
||||
text: "Not Started",
|
||||
type: "default",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Transitions",
|
||||
description:
|
||||
"Smooth transitions between clips. Fade in/out, slide, zoom, dissolve, and custom transition effects.",
|
||||
status: {
|
||||
text: "Not Started",
|
||||
type: "default",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Refine from Here",
|
||||
description:
|
||||
"Once we nail the above, we have a **solid foundation** to build anything. Advanced features, performance optimizations, mobile support, desktop app.",
|
||||
status: {
|
||||
text: "Future",
|
||||
type: "info",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Roadmap - OpenCut",
|
||||
description:
|
||||
"See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
|
||||
openGraph: {
|
||||
title: "OpenCut Roadmap - What's Coming Next",
|
||||
description:
|
||||
"See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "/open-graph/roadmap.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "OpenCut Roadmap",
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "OpenCut Roadmap - What's Coming Next",
|
||||
description:
|
||||
"See what's coming next for OpenCut - the free, open-source video editor that respects your privacy.",
|
||||
images: ["/open-graph/roadmap.jpg"],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RoadmapPage() {
|
||||
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-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
|
||||
</div>
|
||||
<div className="relative container mx-auto px-4 py-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-10">
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut"
|
||||
target="_blank"
|
||||
>
|
||||
<Badge variant="secondary" className="gap-2 mb-6">
|
||||
<GithubIcon className="h-3 w-3" />
|
||||
Open Source
|
||||
</Badge>
|
||||
</Link>
|
||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
|
||||
Roadmap
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
|
||||
What's coming next for OpenCut (last updated: July 14, 2025)
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{roadmapItems.map((item, index) => (
|
||||
<div key={index} className="relative">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg font-medium text-muted-foreground select-none leading-[1.5]">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<div className="flex-1 pt-[2px]">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="font-medium text-lg">{item.title}</h3>
|
||||
<Badge
|
||||
className={cn("shadow-none", {
|
||||
"!bg-green-500 text-white":
|
||||
item.status.type === "complete",
|
||||
"!bg-yellow-500 text-white":
|
||||
item.status.type === "pending",
|
||||
"!bg-blue-500 text-white":
|
||||
item.status.type === "info",
|
||||
"!bg-foreground/10 text-accent-foreground":
|
||||
item.status.type === "default",
|
||||
})}
|
||||
>
|
||||
{item.status.text}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-foreground/70 leading-relaxed">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ className, children, ...props }) => (
|
||||
<a
|
||||
className={cn(
|
||||
"text-primary hover:underline",
|
||||
className
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-foreground">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{item.description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-muted/20">
|
||||
<div className="text-center space-y-4">
|
||||
<h3 className="text-xl font-semibold">Want to Help?</h3>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||
OpenCut is open source and built by the community. Every
|
||||
contribution, no matter how small, helps us build the best
|
||||
free video editor possible.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-6">
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut/blob/main/.github/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-sm px-4 py-2 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<GithubIcon className="h-4 w-4 mr-2" />
|
||||
Start Contributing
|
||||
</Badge>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-sm px-4 py-2 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
Report Issues
|
||||
</Badge>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { GithubIcon } from "@/components/icons";
|
||||
import Link from "next/link";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Terms of Service - OpenCut",
|
||||
description:
|
||||
"OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.",
|
||||
openGraph: {
|
||||
title: "Terms of Service - OpenCut",
|
||||
description:
|
||||
"OpenCut's Terms of Service. Fair, transparent terms for our free and open-source video editor.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function TermsPage() {
|
||||
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-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
|
||||
</div>
|
||||
<div className="relative container mx-auto px-4 py-16">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-10">
|
||||
<Link
|
||||
href="https://github.com/OpenCut-app/OpenCut"
|
||||
target="_blank"
|
||||
>
|
||||
<Badge variant="secondary" className="gap-2 mb-6">
|
||||
<GithubIcon className="h-3 w-3" />
|
||||
Open Source
|
||||
</Badge>
|
||||
</Link>
|
||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
|
||||
Terms of Service
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
|
||||
Fair and transparent terms for our free, open-source video
|
||||
editor.
|
||||
</p>
|
||||
</div>
|
||||
<Card className="bg-background/80 backdrop-blur-sm border-2 border-muted/30">
|
||||
<CardContent className="p-8 text-base leading-relaxed space-y-8">
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Welcome to OpenCut
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
OpenCut is a free, open-source video editor that runs in
|
||||
your browser. By using our service, you agree to these
|
||||
terms. We've designed these terms to be fair and protect
|
||||
both you and our project.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Key principle:</strong> Your content stays on your
|
||||
device. We never claim ownership of your videos or projects.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Your Content, Your Rights
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
<strong>You own everything you create.</strong> OpenCut
|
||||
processes your videos locally on your device, so we never
|
||||
have access to your content. We make no claims to ownership,
|
||||
licensing, or rights over your videos, projects, or any
|
||||
content you create using OpenCut.
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>
|
||||
Your videos remain completely private and under your
|
||||
control
|
||||
</li>
|
||||
<li>
|
||||
You retain all intellectual property rights to your
|
||||
content
|
||||
</li>
|
||||
<li>
|
||||
You can export and use your content however you choose
|
||||
</li>
|
||||
<li>
|
||||
No watermarks, no licensing restrictions from OpenCut
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
How You Can Use OpenCut
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
OpenCut is free for personal and commercial use. You can:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>
|
||||
Create videos for personal, educational, or commercial
|
||||
purposes
|
||||
</li>
|
||||
<li>Use OpenCut for client work and paid projects</li>
|
||||
<li>Share and distribute videos created with OpenCut</li>
|
||||
<li>
|
||||
Modify and distribute the OpenCut software (under MIT
|
||||
license)
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>What we ask:</strong> Don't use OpenCut for illegal
|
||||
activities, harassment, or creating harmful content. Be
|
||||
respectful of others and follow applicable laws.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Account and Service
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
To use certain features, you may create an account:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Provide accurate information when signing up</li>
|
||||
<li>
|
||||
Keep your account secure and don't share credentials
|
||||
</li>
|
||||
<li>You're responsible for activity under your account</li>
|
||||
<li>You can delete your account at any time</li>
|
||||
</ul>
|
||||
<p>
|
||||
OpenCut is provided "as is" without warranties. While we
|
||||
strive for reliability, we can't guarantee uninterrupted
|
||||
service.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Open Source Benefits
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
Because OpenCut is open source, you have additional rights:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>
|
||||
Review our code to see exactly how we handle your data
|
||||
</li>
|
||||
<li>Self-host OpenCut on your own servers</li>
|
||||
<li>Modify the software to suit your needs</li>
|
||||
<li>Contribute improvements back to the community</li>
|
||||
</ul>
|
||||
<p>
|
||||
View our source code and license on{" "}
|
||||
<a
|
||||
href="https://github.com/OpenCut-app/OpenCut"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Third-Party Content
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
When using OpenCut, make sure you have the right to use any
|
||||
content you import:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>
|
||||
Only upload content you own or have permission to use
|
||||
</li>
|
||||
<li>
|
||||
Respect copyright, trademarks, and other intellectual
|
||||
property
|
||||
</li>
|
||||
<li>
|
||||
Don't use copyrighted music, images, or videos without
|
||||
permission
|
||||
</li>
|
||||
<li>
|
||||
You're responsible for any claims related to your content
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Limitations and Liability
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
OpenCut is provided free of charge. To the extent permitted
|
||||
by law:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>We're not liable for any loss of data or content</li>
|
||||
<li>
|
||||
Projects are stored in your browser and may be lost if you
|
||||
clear browser data
|
||||
</li>
|
||||
<li>We're not responsible for how you use the service</li>
|
||||
<li>
|
||||
Our liability is limited to the maximum extent allowed by
|
||||
law
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Since your content stays on your device, we have no way to
|
||||
recover lost projects. Consider exporting important videos
|
||||
when finished editing.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Service Changes
|
||||
</h2>
|
||||
<p className="mb-4">We may update OpenCut and these terms:</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>
|
||||
We'll notify you of significant changes to these terms
|
||||
</li>
|
||||
<li>Continued use means you accept any updates</li>
|
||||
<li>
|
||||
You can always self-host an older version if you prefer
|
||||
</li>
|
||||
<li>
|
||||
Major changes will be discussed with the community on
|
||||
GitHub
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">Termination</h2>
|
||||
<p className="mb-4">
|
||||
You can stop using OpenCut at any time:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
<li>Delete your account through your profile settings</li>
|
||||
<li>Clear your browser data to remove local projects</li>
|
||||
<li>
|
||||
Your content remains yours even if you stop using OpenCut
|
||||
</li>
|
||||
<li>
|
||||
We may suspend accounts for violations of these terms
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Contact and Disputes
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
Questions about these terms or need to report an issue?
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Contact us through our{" "}
|
||||
<a
|
||||
href="https://github.com/OpenCut-app/OpenCut/issues"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
GitHub repository
|
||||
</a>
|
||||
, email us at{" "}
|
||||
<a
|
||||
href="mailto:oss@opencut.app"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
oss@opencut.app
|
||||
</a>
|
||||
, or reach out on{" "}
|
||||
<a
|
||||
href="https://x.com/opencutapp"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
X (Twitter)
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
These terms are governed by applicable law in your
|
||||
jurisdiction. We prefer to resolve disputes through friendly
|
||||
discussion in our open-source community.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="text-sm text-muted-foreground mt-8 pt-8 border-t border-muted/20">
|
||||
Last updated: July 14, 2025
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
export default function WhyNotCapcut() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background px-5">
|
||||
<Header />
|
||||
|
||||
<main className="relative mt-12">
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-96 h-96 bg-gradient-to-br from-muted/20 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/2 -left-40 w-80 h-80 bg-gradient-to-tr from-muted/10 to-transparent rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative container mx-auto px-4 py-16">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="text-center mb-20">
|
||||
<h1 className="text-5xl md:text-6xl font-bold tracking-tight mb-6">
|
||||
Fuck CapCut
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto leading-relaxed">
|
||||
Roasting time, so get ready motherfucker.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto space-y-12">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
Seriously, what the fuck else do you want?
|
||||
</h2>
|
||||
<p className="text-lg mb-6">
|
||||
You probably use CapCut and think your video editing is
|
||||
special. You think your fucking TikTok with 47 transitions and
|
||||
12 different fonts is going to get you some viral fame. You
|
||||
think loading up every goddamn effect in their library makes
|
||||
your content better. Wrong, motherfucker. Let me describe what
|
||||
CapCut actually gives you:
|
||||
</p>
|
||||
<ul className="text-lg space-y-2 mb-6 list-disc list-inside">
|
||||
<li>A paywall every time you breathe</li>
|
||||
<li>Terms of service that steal your shit</li>
|
||||
<li>
|
||||
More "Get Pro" dialogs than a Windows 95 error message
|
||||
</li>
|
||||
<li>
|
||||
Features that disappear behind paywalls while you're fucking
|
||||
using them
|
||||
</li>
|
||||
<li>Bugs disguised as "premium features"</li>
|
||||
</ul>
|
||||
<p className="text-lg mb-6">
|
||||
<strong>Well guess what, motherfucker:</strong>
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
You. Are. Getting. Scammed. Look at this shit. It's a fucking
|
||||
video editor. Why the fuck do you need to pay $20/month just
|
||||
to remove a goddamn watermark? You spent hours editing your
|
||||
video and they slap their logo on it like they fucking made
|
||||
it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
The "Get Pro" dialog is everywhere
|
||||
</h2>
|
||||
<p className="text-lg mb-6">
|
||||
This motherfucking dialog pops up more than ads on a pirated
|
||||
movie site. Want to add a transition? Get Pro. Want to export
|
||||
without their watermark? Get Pro. Want to use more than 2
|
||||
fonts? Get fucking Pro, peasant.
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
Did you seriously think you could edit a video without seeing
|
||||
this dialog 47 times? You click one button and BAM - there it
|
||||
is again, asking for your credit card like a desperate ex
|
||||
asking for money.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
Everything costs money now
|
||||
</h2>
|
||||
<p className="text-lg mb-6">
|
||||
You dumbass. You thought CapCut was free, but no. Free means
|
||||
they let you open the app. Everything else costs money. Basic
|
||||
shake effect? That'll be $20/month. A decent transition that isn't
|
||||
"fade"? Pay up, motherfucker.
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
Here's my favorite piece of bullshit: You import an MP3 file -
|
||||
you know, AUDIO - and try to export. "Sorry, can't export
|
||||
because you're using our premium extract audio feature!"
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
<strong>
|
||||
My MP3 was already fucking audio, you absolute morons.
|
||||
</strong>
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
But wait, there's more! If you drag that same MP3 to their
|
||||
media panel first, then to the timeline, it magically works.
|
||||
This isn't a bug, it's a fucking scam disguised as software
|
||||
engineering.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
Their Terms of Service are insane
|
||||
</h2>
|
||||
<p className="text-lg mb-6">
|
||||
Look at this shit. You upload your content and they basically
|
||||
say "thanks for the free content, we own it now, but if Disney
|
||||
sues anyone, that's your problem."
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
<strong>CapCut's Terms of Service:</strong> We get full rights
|
||||
to use, modify, distribute, and monetize everything you upload
|
||||
- permanently and without paying you shit. But you're still
|
||||
responsible if anything goes wrong.
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
Translation: "We'll make money off your viral video, you
|
||||
handle the lawsuits." Brilliant legal strategy, you fucks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
The editor is actually good
|
||||
</h2>
|
||||
<p className="text-lg mb-6">
|
||||
Here's the thing that makes me want to punch my monitor: the
|
||||
actual video editor is fucking good. It's intuitive, powerful,
|
||||
and anyone can figure it out. When it's not begging for money
|
||||
every 30 seconds, it actually works well.
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
Which makes everything else so much worse. They built
|
||||
something people want to use, then turned it into a digital
|
||||
slot machine. Every click might trigger a payment request.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
This is a video editor. Look at it. You've never seen one
|
||||
before.
|
||||
</h2>
|
||||
<p className="text-lg mb-6">
|
||||
Like the person who's never used software that doesn't
|
||||
constantly beg for money, you have no fucking idea what a
|
||||
video editor should be. All you've ever seen are predatory
|
||||
apps disguised as creative tools.
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
A real video editor lets you edit videos. It doesn't steal
|
||||
your content. It doesn't pop up payment dialogs every 5
|
||||
seconds. It doesn't charge you separately for basic features
|
||||
that should be free.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-6">
|
||||
Yes, this is fucking satire, you fuck
|
||||
</h2>
|
||||
<p className="text-lg mb-6">
|
||||
I'm not actually saying all video editors should be basic as
|
||||
shit. What I'm saying is that all the problems we have with
|
||||
video editing apps are{" "}
|
||||
<strong>ones they create themselves</strong>. Video editors
|
||||
aren't broken by default - they edit videos, export them, and
|
||||
let you use basic features without constantly begging for
|
||||
money. CapCut breaks them. They turn them into payment
|
||||
processors with video editing as a side feature.
|
||||
</p>
|
||||
<p className="text-lg">
|
||||
<em>"Good software gets out of your way."</em>
|
||||
<br />- Some smart motherfucker who definitely wasn't working
|
||||
at CapCut
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { Button } from "./ui/button";
|
||||
import { BackgroundIcon } from "./icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { colors } from "@/data/colors";
|
||||
import { useProjectStore } from "@/stores/project-store";
|
||||
import { PipetteIcon } from "lucide-react";
|
||||
|
||||
type BackgroundTab = "color" | "blur";
|
||||
|
||||
export function BackgroundSettings() {
|
||||
const { activeProject, updateBackgroundType } = useProjectStore();
|
||||
|
||||
// ✅ Good: derive activeTab from activeProject during rendering
|
||||
const activeTab = activeProject?.backgroundType || "color";
|
||||
|
||||
const handleColorSelect = (color: string) => {
|
||||
updateBackgroundType("color", { backgroundColor: color });
|
||||
};
|
||||
|
||||
const handleBlurSelect = (blurIntensity: number) => {
|
||||
updateBackgroundType("blur", { blurIntensity });
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: "Color",
|
||||
value: "color",
|
||||
},
|
||||
{
|
||||
label: "Blur",
|
||||
value: "blur",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="icon"
|
||||
className="!size-4 border border-muted-foreground"
|
||||
>
|
||||
<BackgroundIcon className="!size-3" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex flex-col items-start w-[20rem] h-[16rem] overflow-hidden p-0">
|
||||
<div className="flex items-center justify-between w-full gap-2 z-10 bg-popover p-3">
|
||||
<h2 className="text-sm">Background</h2>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{tabs.map((tab) => (
|
||||
<span
|
||||
key={tab.value}
|
||||
onClick={() => {
|
||||
// Switch to the background type when clicking tabs
|
||||
if (tab.value === "color") {
|
||||
updateBackgroundType("color", {
|
||||
backgroundColor:
|
||||
activeProject?.backgroundColor || "#000000",
|
||||
});
|
||||
} else {
|
||||
updateBackgroundType("blur", {
|
||||
blurIntensity: activeProject?.blurIntensity || 8,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"text-muted-foreground cursor-pointer",
|
||||
activeTab === tab.value && "text-foreground"
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{activeTab === "color" ? (
|
||||
<ColorView
|
||||
selectedColor={activeProject?.backgroundColor || "#000000"}
|
||||
onColorSelect={handleColorSelect}
|
||||
/>
|
||||
) : (
|
||||
<BlurView
|
||||
selectedBlur={activeProject?.blurIntensity || 8}
|
||||
onBlurSelect={handleBlurSelect}
|
||||
/>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function ColorView({
|
||||
selectedColor,
|
||||
onColorSelect,
|
||||
}: {
|
||||
selectedColor: string;
|
||||
onColorSelect: (color: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<div className="absolute top-8 left-0 w-[calc(100%-1rem)] h-12 bg-gradient-to-b from-popover to-transparent pointer-events-none"></div>
|
||||
<div className="grid grid-cols-4 gap-2 w-full h-full p-3 pt-0 overflow-auto">
|
||||
<div className="w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary flex items-center justify-center">
|
||||
<PipetteIcon className="size-4" />
|
||||
</div>
|
||||
{colors.map((color) => (
|
||||
<ColorItem
|
||||
key={color}
|
||||
color={color}
|
||||
isSelected={color === selectedColor}
|
||||
onClick={() => onColorSelect(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ColorItem({
|
||||
color,
|
||||
isSelected,
|
||||
onClick,
|
||||
}: {
|
||||
color: string;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full aspect-square rounded-sm cursor-pointer hover:border-2 hover:border-primary",
|
||||
isSelected && "border-2 border-primary"
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BlurView({
|
||||
selectedBlur,
|
||||
onBlurSelect,
|
||||
}: {
|
||||
selectedBlur: number;
|
||||
onBlurSelect: (blurIntensity: number) => void;
|
||||
}) {
|
||||
const blurLevels = [
|
||||
{ label: "Light", value: 4 },
|
||||
{ label: "Medium", value: 8 },
|
||||
{ label: "Heavy", value: 18 },
|
||||
];
|
||||
const blurImage =
|
||||
"https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2 w-full p-3 pt-0">
|
||||
{blurLevels.map((blur) => (
|
||||
<div
|
||||
key={blur.value}
|
||||
className={cn(
|
||||
"w-full aspect-square rounded-sm cursor-pointer hover:border-2 hover:border-primary relative overflow-hidden",
|
||||
selectedBlur === blur.value && "border-2 border-primary"
|
||||
)}
|
||||
onClick={() => onBlurSelect(blur.value)}
|
||||
>
|
||||
<Image
|
||||
src={blurImage}
|
||||
alt={`Blur preview ${blur.label}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
style={{ filter: `blur(${blur.value}px)` }}
|
||||
/>
|
||||
<div className="absolute bottom-1 left-1 right-1 text-center">
|
||||
<span className="text-xs text-white bg-black/50 px-1 rounded">
|
||||
{blur.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||